chamber 2.13.1 → 2.14.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 (49) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/README.md +101 -26
  5. data/lib/chamber.rb +72 -10
  6. data/lib/chamber/adapters/cloud/circle_ci.rb +16 -13
  7. data/lib/chamber/adapters/cloud/heroku.rb +40 -13
  8. data/lib/chamber/binary/circle_ci.rb +25 -12
  9. data/lib/chamber/binary/heroku.rb +31 -12
  10. data/lib/chamber/binary/runner.rb +37 -27
  11. data/lib/chamber/binary/travis.rb +5 -3
  12. data/lib/chamber/commands/base.rb +10 -16
  13. data/lib/chamber/commands/cloud/base.rb +3 -3
  14. data/lib/chamber/commands/cloud/pull.rb +2 -2
  15. data/lib/chamber/commands/cloud/push.rb +7 -7
  16. data/lib/chamber/commands/comparable.rb +2 -2
  17. data/lib/chamber/commands/compare.rb +6 -9
  18. data/lib/chamber/commands/initialize.rb +26 -22
  19. data/lib/chamber/commands/securable.rb +9 -9
  20. data/lib/chamber/commands/secure.rb +2 -2
  21. data/lib/chamber/commands/show.rb +8 -8
  22. data/lib/chamber/commands/sign.rb +2 -2
  23. data/lib/chamber/commands/verify.rb +2 -2
  24. data/lib/chamber/configuration.rb +6 -3
  25. data/lib/chamber/context_resolver.rb +8 -7
  26. data/lib/chamber/encryption_methods/ssl.rb +12 -12
  27. data/lib/chamber/file.rb +16 -14
  28. data/lib/chamber/file_set.rb +18 -8
  29. data/lib/chamber/files/signature.rb +16 -14
  30. data/lib/chamber/filters/decryption_filter.rb +12 -10
  31. data/lib/chamber/filters/encryption_filter.rb +8 -8
  32. data/lib/chamber/filters/environment_filter.rb +12 -14
  33. data/lib/chamber/filters/failed_decryption_filter.rb +6 -6
  34. data/lib/chamber/filters/insecure_filter.rb +3 -3
  35. data/lib/chamber/filters/namespace_filter.rb +5 -5
  36. data/lib/chamber/filters/secure_filter.rb +5 -5
  37. data/lib/chamber/filters/translate_secure_keys_filter.rb +5 -5
  38. data/lib/chamber/instance.rb +37 -21
  39. data/lib/chamber/key_pair.rb +7 -7
  40. data/lib/chamber/keys/base.rb +13 -13
  41. data/lib/chamber/keys/decryption.rb +3 -3
  42. data/lib/chamber/keys/encryption.rb +3 -3
  43. data/lib/chamber/namespace_set.rb +2 -4
  44. data/lib/chamber/settings.rb +45 -43
  45. data/lib/chamber/types/secured.rb +8 -10
  46. data/lib/chamber/version.rb +1 -1
  47. data/templates/settings.yml +2 -0
  48. metadata +24 -26
  49. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f29fb224fb009c2d3cda5c3429024a2246fb9ab1893def95a984b087fe77849
4
- data.tar.gz: 281ad1fe8607f1b0c887cfcbd2bb736a82d6d51fd075bbef1e9204bd2d0bb00e
3
+ metadata.gz: 5bf2d68260ae1fa27b7a2befdc5ff274c34477ff3bf584027d39747fbcd5a489
4
+ data.tar.gz: 63f97b11849eb6e800b5f4e4f0ae49ad57d1e92cd942f114d417dc8c980a40a4
5
5
  SHA512:
6
- metadata.gz: 4ac5037eb08def3e9bb7304ebd485917246f83ce9d1db7a605425f4f015f70cb908ac8b3544c1b28cbff2445478be1764a34b05e76a2a33f93cadfcedffd6398
7
- data.tar.gz: 8be1ebdd092c55a5c8a40e7de6320db810728c13411b842f2a060b38c770835e34c763b3220ddc1dd5e614d53b4cd52f7d26725a805861756783dbfe7f780b23
6
+ metadata.gz: 8a8f5f026bee9a0380410ee6ed8c4e563f0c465b56a0d111d10f8ef35eb986f4e3cfeb7acfaa283cfcc7380135026d63f4edbfd3649ea4bfc543b91410f1b155
7
+ data.tar.gz: 9d012efb86e89a1a78cd39f0bd54200d0508394b46a7b66b2144b645bcd86f17d25f1f2ad3d2df8c3cab355f1669903b17d8dc8de024d520b854022a6e32d892
Binary file
data.tar.gz.sig CHANGED
Binary file
data/README.md CHANGED
@@ -1,5 +1,33 @@
1
- # Chamber
2
- [![Gem Version](https://img.shields.io/gem/v/chamber.svg)](https://rubygems.org/gems/chamber) ![Rubygems Rank Overall](https://img.shields.io/gem/rt/chamber.svg) ![Rubygems Rank Daily](https://img.shields.io/gem/rd/chamber.svg) ![Rubygems Downloads](https://img.shields.io/gem/dv/chamber/stable.svg) [![Build Status](https://img.shields.io/travis/thekompanee/chamber/master.svg)](http://travis-ci.org/thekompanee/chamber) [![Code Climate](https://codeclimate.com/github/thekompanee/chamber.svg)](https://codeclimate.com/github/thekompanee/chamber) [![Code Climate](https://codeclimate.com/github/thekompanee/chamber/coverage.svg)](https://codeclimate.com/github/thekompanee/chamber)
1
+ Chamber
2
+ ================================================================================
3
+
4
+ <div align="center">
5
+ <a href="https://rubygems.org/gems/chamber" alt="RubyGems Version">
6
+ <img src="https://img.shields.io/gem/v/chamber.svg?style=flat-square&label=current-version" alt="RubyGems Version" />
7
+ </a>
8
+
9
+ <a href="https://rubygems.org/gems/chamber" alt="RubyGems Rank Overall">
10
+ <img src="https://img.shields.io/gem/rt/chamber.svg?style=flat-square&label=total-rank" alt="RubyGems Rank Overall" />
11
+ </a>
12
+
13
+ <a href="https://rubygems.org/gems/chamber" alt="RubyGems Rank Daily">
14
+ <img src="https://img.shields.io/gem/rd/chamber.svg?style=flat-square&label=daily-rank" alt="RubyGems Rank Daily" />
15
+ </a>
16
+
17
+ <a href="https://rubygems.org/gems/chamber" alt="RubyGems Downloads">
18
+ <img src="https://img.shields.io/gem/dt/chamber.svg?style=flat-square&label=total-downloads" alt="RubyGems Downloads" />
19
+ </a>
20
+
21
+ <a href="https://github.com/thekompanee/chamber/actions?query=workflow%3ABuild" alt="Build Status">
22
+ <img src="https://img.shields.io/github/workflow/status/thekompanee/chamber/Build?label=CI&style=flat-square&logo=github" alt="Build Status" />
23
+ </a>
24
+
25
+ <a href="#" alt="Maintainability">
26
+ <img src="https://img.shields.io/codeclimate/maintainability/thekompanee/chamber?style=flat-square&label=grade" alt="Maintainability" />
27
+ </a>
28
+ </div>
29
+
30
+ <br>
3
31
 
4
32
  Chamber is the auto-encrypting, extremely organizable, Heroku-loving,
5
33
  CLI-having, non-extra-repo-needing, non-Rails-specific-ing, CI-serving
@@ -9,17 +37,73 @@ We looked at all of the options out there and thought something was still
9
37
  missing, so we wrote Chamber. We made it with lots of ❤ and we hope you like it
10
38
  as much as we do.
11
39
 
12
- ## What Sets Chamber Apart?
40
+ What Sets Chamber Apart
41
+ --------------------------------------------------------------------------------
13
42
 
14
43
  For an idea of how Chamber compares to other popular libraries, check out our
15
44
  [Gem Comparison][comparison].
16
45
 
17
- ## Basic Usage
46
+ Basic Usage
47
+ --------------------------------------------------------------------------------
48
+
49
+ Before starting this guide, make sure you [install chamber][installation].
50
+
51
+ Once your app is initialized, you should have a `settings.yml` file somewhere.
52
+ A lot of times it's the root of your project and sometimes it's in a framework
53
+ specific location.
54
+
55
+ Inside of here you can define any settings you'd like like so:
56
+
57
+ ```yaml
58
+ # settings.yml
59
+
60
+ smtp_username: 'my_username'
61
+ smtp_password: 'my_password'
62
+ ```
63
+
64
+ From there you can access your settings by using the special `Chamber.env`
65
+ constant.
66
+
67
+ ```ruby
68
+ Chamber.env.smtp_password
69
+ # => 'my_password'
70
+ ```
71
+
72
+ If you want to encrypt a setting, prefix the setting name with `_secure_` like
73
+ so:
74
+
75
+ ```ruby
76
+ # settings.yml
77
+
78
+ smtp_username: 'my_username'
79
+ _secure_smtp_password: 'my_password'
80
+ ```
81
+
82
+ And then run `chamber secure`. Your settings file will have an encrypted value:
83
+
84
+ ```ruby
85
+ # settings.yml
86
+
87
+ smtp_username: 'my_username'
88
+ _secure_smtp_password: JL5hAVux4tERpv49QPWxy9H0VC2Rnk7V8/e8+1XOwPcXcoH/a7Lh253UY/v9m8nI/Onb+ZG9nZ082J4M/BmLa+f7jwMEwufIqbUhUah9eKIW8xcxlppBYpl7JVGf2HJF5TfCN44gMQNgGNzboCQXKqRyeGFm4u772Sg9V2gEx/q7qJ6F4jg7v/cltCFLmJfXA2SHA5Dai4p9L4IvMVVJGm34k5j7KOegNqpVWs2RY99cagjPuzc9VM2XSUsXgqcUJdmH8YtPW8Kqkyg0oYlRh6VQWABlWXwTZz74QjTTjqtqfoELIoFTMBDh+cCvuUTAE5m06LhlqauVrB4UnBsd5g==
89
+ ```
90
+
91
+ which you still access the same way because Chamber handles the decryption for
92
+ you:
93
+
94
+ ```ruby
95
+ Chamber.env.smtp_password
96
+ # => 'my_password'
97
+ ```
98
+
99
+ Full Reference
100
+ --------------------------------------------------------------------------------
18
101
 
19
- You can view our Basic Usage Guide [here][basic-usage]. Otherwise, for the full
102
+ There's so much to Chamber, we couldn't put it all in the README. For the full
20
103
  Chamber guide, visit the [wiki][wiki].
21
104
 
22
- ## Credits
105
+ Credits
106
+ --------------------------------------------------------------------------------
23
107
 
24
108
  Chamber was written by [Jeff Felchner][jeff-profile] and
25
109
  [Mark McEahern][mark-profile]
@@ -30,27 +114,18 @@ Chamber is maintained and funded by [The Kompanee, Ltd.][kompanee-site]
30
114
 
31
115
  The names and logos for The Kompanee are trademarks of The Kompanee, Ltd.
32
116
 
33
- ## License
117
+ License
118
+ --------------------------------------------------------------------------------
34
119
 
35
- Chamber is Copyright © 2014-2019 Jeff Felchner and Mark McEahern. It is free
120
+ Chamber is Copyright © 2014-2021 Jeff Felchner and Mark McEahern. It is free
36
121
  software, and may be redistributed under the terms specified in the
37
122
  [LICENSE][license] file.
38
123
 
39
- [accessing]: https://github.com/thekompanee/chamber/wiki/Accessing-Settings
40
- [basic-usage]: https://github.com/thekompanee/chamber/wiki/Basic-Usage
41
- [cli]: https://github.com/thekompanee/chamber/wiki/CLI-Overview
42
- [commit-hook]: https://github.com/thekompanee/chamber/wiki/Git-Commit-Hooks
43
- [comparison]: https://github.com/thekompanee/chamber/wiki/Gem-Comparison
44
- [encryption]: https://github.com/thekompanee/chamber/wiki/Encryption-Basics
45
- [env-vars]: https://github.com/thekompanee/chamber/wiki/Environment-Variables
46
- [heroku]: https://github.com/thekompanee/chamber/wiki/Heroku
47
- [inch]: https://inch-ci.org/github/thekompanee/chamber
48
- [jeff-profile]: https://github.com/jfelchner
49
- [kompanee-logo]: https://kompanee-public-assets.s3.amazonaws.com/readmes/kompanee-horizontal-black.png
50
- [kompanee-site]: http://www.thekompanee.com
51
- [license]: https://github.com/thekompanee/chamber/blob/master/LICENSE.txt
52
- [mark-profile]: https://github.com/m5rk
53
- [namespace-keys]: https://github.com/thekompanee/chamber/wiki/Namespaced-Key-Pairs
54
- [plain-ruby]: https://github.com/thekompanee/chamber/wiki/Installation#in-a-ruby-project-or-ruby-gem
55
- [travis]: https://github.com/thekompanee/chamber/wiki/TravisCI
56
- [wiki]: https://github.com/thekompanee/chamber/wiki
124
+ [comparison]: https://github.com/thekompanee/chamber/wiki/Gem-Comparison
125
+ [jeff-profile]: https://github.com/jfelchner
126
+ [kompanee-logo]: https://kompanee-public-assets.s3.amazonaws.com/readmes/kompanee-horizontal-black.png
127
+ [kompanee-site]: http://www.thekompanee.com
128
+ [license]: https://github.com/thekompanee/chamber/blob/master/LICENSE.txt
129
+ [mark-profile]: https://github.com/m5rk
130
+ [wiki]: https://github.com/thekompanee/chamber/wiki
131
+ [installation]: https://github.com/thekompanee/chamber/wiki/Installation
@@ -7,12 +7,8 @@ require 'chamber/rails'
7
7
  module Chamber
8
8
  attr_writer :instance
9
9
 
10
- def load(options = {})
11
- self.instance = Instance.new(options)
12
- end
13
-
14
- def to_s(options = {})
15
- instance.to_s(options)
10
+ def load(**args)
11
+ self.instance = Instance.new(**args)
16
12
  end
17
13
 
18
14
  def env
@@ -20,7 +16,61 @@ module Chamber
20
16
  end
21
17
 
22
18
  def instance
23
- @instance ||= Instance.new({})
19
+ @instance ||= Instance.new
20
+ end
21
+
22
+ def [](key)
23
+ instance.[](key)
24
+ end
25
+
26
+ def configuration
27
+ instance.configuration
28
+ end
29
+
30
+ def decrypt(value, **args)
31
+ instance.decrypt(value, **args)
32
+ end
33
+
34
+ def encrypt(value, **args)
35
+ instance.encrypt(value, **args)
36
+ end
37
+
38
+ def files
39
+ instance.files
40
+ end
41
+
42
+ def filenames
43
+ instance.filenames
44
+ end
45
+
46
+ def namespaces
47
+ instance.namespaces
48
+ end
49
+
50
+ def secure
51
+ instance.secure
52
+ end
53
+
54
+ def sign
55
+ instance.sign
56
+ end
57
+
58
+ def verify
59
+ instance.verify
60
+ end
61
+
62
+ def to_environment
63
+ instance.to_environment
64
+ end
65
+
66
+ def to_hash
67
+ instance.to_hash
68
+ end
69
+
70
+ def to_s(**args)
71
+ return '' unless @instance
72
+
73
+ instance.to_s(**args)
24
74
  end
25
75
 
26
76
  def method_missing(name, *args)
@@ -33,11 +83,23 @@ module Chamber
33
83
  instance.respond_to?(name, include_private)
34
84
  end
35
85
 
36
- module_function :load,
37
- :to_s,
86
+ module_function :[],
87
+ :configuration,
88
+ :decrypt,
89
+ :encrypt,
38
90
  :env,
91
+ :filenames,
92
+ :files,
39
93
  :instance,
40
94
  :instance=,
95
+ :load,
41
96
  :method_missing,
42
- :respond_to_missing?
97
+ :namespaces,
98
+ :respond_to_missing?,
99
+ :secure,
100
+ :sign,
101
+ :to_environment,
102
+ :to_hash,
103
+ :to_s,
104
+ :verify
43
105
  end
@@ -17,11 +17,11 @@ class CircleCi
17
17
  :username,
18
18
  :vcs_type
19
19
 
20
- def initialize(options = {})
21
- self.api_token = options.fetch(:api_token)
22
- self.project = options.fetch(:project)
23
- self.username = options.fetch(:username)
24
- self.vcs_type = options.fetch(:vcs_type)
20
+ def initialize(api_token:, project:, username:, vcs_type:)
21
+ self.api_token = api_token
22
+ self.project = project
23
+ self.username = username
24
+ self.vcs_type = vcs_type
25
25
  end
26
26
 
27
27
  def add_environment_variable(name, value)
@@ -39,18 +39,21 @@ class CircleCi
39
39
  response['name']
40
40
  end
41
41
 
42
+ # rubocop:disable Layout/MultilineAssignmentLayout
42
43
  def environment_variables
43
- @environment_variables ||= begin
44
- request = ::Net::HTTP::Get.new(request_uri(resource: 'envvar'))
44
+ @environment_variables ||= \
45
+ begin
46
+ request = ::Net::HTTP::Get.new(request_uri(resource: 'envvar'))
45
47
 
46
- request.basic_auth api_token, ''
47
- request['Content-Type'] = 'application/json'
48
+ request.basic_auth api_token, ''
49
+ request['Content-Type'] = 'application/json'
48
50
 
49
- ::JSON
50
- .parse(response(request).body)
51
- .each_with_object({}) { |e, m| m[e['name']] = e['value'] }
52
- end
51
+ ::JSON
52
+ .parse(response(request).body)
53
+ .each_with_object({}) { |e, m| m[e['name']] = e['value'] }
54
+ end
53
55
  end
56
+ # rubocop:enable Layout/MultilineAssignmentLayout
54
57
 
55
58
  def remove_environment_variable(name)
56
59
  request = ::Net::HTTP::Delete.new(request_uri(resource: "envvar/#{name}"))
@@ -8,38 +8,65 @@ module Chamber
8
8
  module Adapters
9
9
  module Cloud
10
10
  class Heroku
11
- attr_accessor :app
11
+ API_HOST = 'api.heroku.com'
12
+ API_PORT = 443
13
+ API_BASE_URI = ''
12
14
 
13
- def initialize(options = {})
14
- self.app = options.fetch(:app)
15
+ attr_accessor :api_token,
16
+ :app
17
+
18
+ def initialize(api_token:, app:)
19
+ self.api_token = api_token
20
+ self.app = app
15
21
  end
16
22
 
17
- def add_environment_variable(name, value)
18
- value = value.shellescape unless value =~ /\n/
23
+ def add_environment_variable(name, value) # rubocop:disable Metrics/AbcSize
24
+ value = value.gsub(/\n/, '\n') if value
25
+ request = ::Net::HTTP::Patch.new(config_vars_uri)
26
+
27
+ request['Authorization'] = "Bearer #{api_token}"
28
+ request['Accept'] = 'application/vnd.heroku+json; version=3'
29
+ request['Content-Type'] = 'application/json'
30
+ request.body = ::JSON.dump(Hash[name, value])
19
31
 
20
- response = heroku(%Q{config:set #{name}="#{value}"})
32
+ response = ::JSON.parse(response(request).body)
21
33
 
22
- fail NameError, "The variable name '#{name}' is invalid" if response.match?(/invalid/)
34
+ fail NameError, response['message'] if response['message']
23
35
 
24
36
  response
25
37
  end
26
38
 
27
39
  def environment_variables
28
- @environment_variables ||= ::JSON.parse(heroku('config --json'))
40
+ request = ::Net::HTTP::Get.new(config_vars_uri)
41
+
42
+ request['Authorization'] = "Bearer #{api_token}"
43
+ request['Accept'] = 'application/vnd.heroku+json; version=3'
44
+
45
+ response = ::JSON.parse(response(request).body)
46
+
47
+ fail NameError, response['message'] if response['message']
48
+
49
+ response
29
50
  end
30
51
 
31
52
  def remove_environment_variable(name)
32
- heroku("config:unset #{name}")
53
+ add_environment_variable(name, nil)
33
54
  end
34
55
 
35
56
  private
36
57
 
37
- def heroku(command)
38
- Bundler.with_clean_env { `heroku #{command}#{app_option} 2>&1` }
58
+ def config_vars_uri
59
+ "#{API_BASE_URI}/apps/#{app}/config-vars"
60
+ end
61
+
62
+ def response(request)
63
+ connection.request(request)
39
64
  end
40
65
 
41
- def app_option
42
- app ? " --app='#{app}'" : ''
66
+ def connection
67
+ @connection ||= ::Net::HTTP.new(API_HOST, API_PORT).tap do |conn|
68
+ conn.use_ssl = true
69
+ end
43
70
  end
44
71
  end
45
72
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'thor'
4
+ require 'chamber/core_ext/hash'
4
5
  require 'chamber/commands/cloud/clear'
5
6
  require 'chamber/commands/cloud/push'
6
7
  require 'chamber/commands/cloud/pull'
@@ -36,8 +37,9 @@ class CircleCi < Thor
36
37
  desc: 'The type of VCS your project is using.',
37
38
  enum: %w{github bitbucket}
38
39
 
39
- desc 'clear', 'Removes all CircleCi environment variables which match settings that ' \
40
- 'Chamber knows about'
40
+ desc 'clear',
41
+ 'Removes all CircleCi environment variables which match settings that Chamber ' \
42
+ 'knows about'
41
43
 
42
44
  method_option :dry_run,
43
45
  type: :boolean,
@@ -46,11 +48,14 @@ class CircleCi < Thor
46
48
  'would change if cleared'
47
49
 
48
50
  def clear
49
- Commands::Cloud::Clear.call(options.merge(shell: self, adapter: 'circle_ci'))
51
+ Commands::Cloud::Clear.call(**options
52
+ .transform_keys(&:to_sym)
53
+ .merge(shell: self, adapter: 'circle_ci'))
50
54
  end
51
55
 
52
- desc 'push', 'Sends settings to CircleCi so that they may be used in the application ' \
53
- 'once it is deployed'
56
+ desc 'push',
57
+ 'Sends settings to CircleCi so that they may be used in the application ' \
58
+ 'once it is deployed'
54
59
 
55
60
  method_option :dry_run,
56
61
  type: :boolean,
@@ -75,11 +80,14 @@ class CircleCi < Thor
75
80
  'will be pushed'
76
81
 
77
82
  def push
78
- Commands::Cloud::Push.call(options.merge(shell: self, adapter: 'circle_ci'))
83
+ Commands::Cloud::Push.call(**options
84
+ .transform_keys(&:to_sym)
85
+ .merge(shell: self, adapter: 'circle_ci'))
79
86
  end
80
87
 
81
- desc 'pull', 'Retrieves the environment variables for the application and stores ' \
82
- 'them in a temporary file'
88
+ desc 'pull',
89
+ 'Retrieves the environment variables for the application and stores them in a ' \
90
+ 'temporary file'
83
91
 
84
92
  method_option :into,
85
93
  type: :string,
@@ -87,11 +95,14 @@ class CircleCi < Thor
87
95
  'stored. This file WILL BE OVERRIDDEN.'
88
96
 
89
97
  def pull
90
- Commands::Cloud::Pull.call(options.merge(shell: self, adapter: 'circle_ci'))
98
+ Commands::Cloud::Pull.call(**options
99
+ .transform_keys(&:to_sym)
100
+ .merge(shell: self, adapter: 'circle_ci'))
91
101
  end
92
102
 
93
- desc 'compare', 'Displays the difference between what is currently stored in the ' \
94
- 'CircleCi application\'s config and what Chamber knows about locally'
103
+ desc 'compare',
104
+ 'Displays the difference between what is currently stored in the ' \
105
+ 'CircleCi application\'s config and what Chamber knows about locally'
95
106
 
96
107
  method_option :only_sensitive,
97
108
  type: :boolean,
@@ -102,7 +113,9 @@ class CircleCi < Thor
102
113
  'which are marked as "_secure"'
103
114
 
104
115
  def compare
105
- Commands::Cloud::Compare.call(options.merge(shell: self, adapter: 'circle_ci'))
116
+ Commands::Cloud::Compare.call(**options
117
+ .transform_keys(&:to_sym)
118
+ .merge(shell: self, adapter: 'circle_ci'))
106
119
  end
107
120
  end
108
121
  end