r10k 3.10.0 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -10
  3. data/CHANGELOG.mkd +33 -0
  4. data/README.mkd +6 -0
  5. data/doc/dynamic-environments/configuration.mkd +25 -7
  6. data/doc/dynamic-environments/usage.mkd +26 -0
  7. data/doc/puppetfile.mkd +18 -5
  8. data/integration/Rakefile +2 -0
  9. data/integration/tests/basic_functionality/basic_deployment.rb +176 -0
  10. data/integration/tests/user_scenario/basic_workflow/single_env_purge_unmanaged_modules.rb +15 -13
  11. data/integration/tests/user_scenario/complex_workflow/multi_env_add_change_remove.rb +3 -3
  12. data/integration/tests/user_scenario/complex_workflow/multi_env_remove_re-add.rb +3 -3
  13. data/integration/tests/user_scenario/complex_workflow/multi_env_unamanaged.rb +3 -3
  14. data/lib/r10k/action/base.rb +1 -1
  15. data/lib/r10k/action/deploy/deploy_helpers.rb +4 -0
  16. data/lib/r10k/action/deploy/display.rb +1 -1
  17. data/lib/r10k/action/deploy/environment.rb +19 -9
  18. data/lib/r10k/action/deploy/module.rb +41 -11
  19. data/lib/r10k/action/puppetfile/check.rb +7 -5
  20. data/lib/r10k/action/puppetfile/install.rb +22 -16
  21. data/lib/r10k/action/puppetfile/purge.rb +12 -9
  22. data/lib/r10k/action/runner.rb +40 -4
  23. data/lib/r10k/action/visitor.rb +3 -0
  24. data/lib/r10k/cli/deploy.rb +5 -0
  25. data/lib/r10k/cli/puppetfile.rb +0 -1
  26. data/lib/r10k/content_synchronizer.rb +16 -4
  27. data/lib/r10k/environment/bare.rb +4 -7
  28. data/lib/r10k/environment/base.rb +64 -11
  29. data/lib/r10k/environment/plain.rb +16 -0
  30. data/lib/r10k/environment/with_modules.rb +6 -10
  31. data/lib/r10k/environment.rb +1 -0
  32. data/lib/r10k/errors.rb +5 -0
  33. data/lib/r10k/git/rugged/credentials.rb +77 -0
  34. data/lib/r10k/git/stateful_repository.rb +8 -0
  35. data/lib/r10k/git.rb +3 -0
  36. data/lib/r10k/initializers.rb +14 -7
  37. data/lib/r10k/logging.rb +78 -1
  38. data/lib/r10k/module/base.rb +42 -1
  39. data/lib/r10k/module/definition.rb +64 -0
  40. data/lib/r10k/module/forge.rb +11 -2
  41. data/lib/r10k/module/git.rb +23 -1
  42. data/lib/r10k/module/local.rb +6 -3
  43. data/lib/r10k/module/svn.rb +11 -0
  44. data/lib/r10k/module.rb +20 -2
  45. data/lib/r10k/module_loader/puppetfile/dsl.rb +8 -3
  46. data/lib/r10k/module_loader/puppetfile.rb +109 -28
  47. data/lib/r10k/puppetfile.rb +11 -13
  48. data/lib/r10k/settings/definition.rb +1 -1
  49. data/lib/r10k/settings.rb +89 -1
  50. data/lib/r10k/source/yaml.rb +1 -1
  51. data/lib/r10k/util/purgeable.rb +6 -2
  52. data/lib/r10k/util/setopts.rb +2 -0
  53. data/lib/r10k/util/subprocess.rb +1 -0
  54. data/lib/r10k/version.rb +1 -1
  55. data/locales/r10k.pot +168 -68
  56. data/r10k.gemspec +2 -0
  57. data/r10k.yaml.example +28 -0
  58. data/spec/fixtures/unit/action/r10k_logging.yaml +12 -0
  59. data/spec/fixtures/unit/puppetfile/forge-override/Puppetfile +8 -0
  60. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile +10 -0
  61. data/spec/fixtures/unit/puppetfile/various-modules/Puppetfile.new +10 -0
  62. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/managed_symlink_file +1 -0
  63. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/{managed_subdir_2 → subdir_allowlisted_2}/ignored_1 +0 -0
  64. data/spec/fixtures/unit/util/purgeable/managed_one/managed_subdir_1/unmanaged_symlink_dir +1 -0
  65. data/spec/fixtures/unit/util/purgeable/managed_one/managed_symlink_dir +1 -0
  66. data/spec/fixtures/unit/util/purgeable/managed_one/unmanaged_symlink_file +1 -0
  67. data/spec/integration/util/purageable_spec.rb +41 -0
  68. data/spec/r10k-mocks/mock_env.rb +3 -0
  69. data/spec/r10k-mocks/mock_source.rb +7 -3
  70. data/spec/unit/action/deploy/environment_spec.rb +96 -30
  71. data/spec/unit/action/deploy/module_spec.rb +217 -51
  72. data/spec/unit/action/puppetfile/check_spec.rb +17 -5
  73. data/spec/unit/action/puppetfile/install_spec.rb +42 -36
  74. data/spec/unit/action/puppetfile/purge_spec.rb +15 -17
  75. data/spec/unit/action/runner_spec.rb +132 -9
  76. data/spec/unit/environment/bare_spec.rb +13 -0
  77. data/spec/unit/environment/base_spec.rb +30 -17
  78. data/spec/unit/environment/git_spec.rb +2 -2
  79. data/spec/unit/environment/plain_spec.rb +8 -0
  80. data/spec/unit/environment/svn_spec.rb +4 -3
  81. data/spec/unit/environment/with_modules_spec.rb +3 -2
  82. data/spec/unit/git/rugged/credentials_spec.rb +29 -0
  83. data/spec/unit/git/stateful_repository_spec.rb +5 -0
  84. data/spec/unit/module/base_spec.rb +54 -8
  85. data/spec/unit/module/forge_spec.rb +51 -4
  86. data/spec/unit/module/git_spec.rb +67 -9
  87. data/spec/unit/module/svn_spec.rb +35 -5
  88. data/spec/unit/module_loader/puppetfile_spec.rb +108 -33
  89. data/spec/unit/module_spec.rb +12 -1
  90. data/spec/unit/puppetfile_spec.rb +33 -3
  91. data/spec/unit/settings_spec.rb +43 -2
  92. data/spec/unit/util/purgeable_spec.rb +22 -11
  93. metadata +31 -3
@@ -4,39 +4,85 @@ require 'r10k/module/base'
4
4
  describe R10K::Module::Base do
5
5
  describe "parsing the title" do
6
6
  it "parses titles with no owner" do
7
- m = described_class.new('eight_hundred', '/moduledir', [])
7
+ m = described_class.new('eight_hundred', '/moduledir', {})
8
8
  expect(m.name).to eq 'eight_hundred'
9
9
  expect(m.owner).to be_nil
10
10
  end
11
11
 
12
12
  it "parses forward slash separated titles" do
13
- m = described_class.new('branan/eight_hundred', '/moduledir', [])
13
+ m = described_class.new('branan/eight_hundred', '/moduledir', {})
14
14
  expect(m.name).to eq 'eight_hundred'
15
15
  expect(m.owner).to eq 'branan'
16
16
  end
17
17
 
18
18
  it "parses hyphen separated titles" do
19
- m = described_class.new('branan-eight_hundred', '/moduledir', [])
19
+ m = described_class.new('branan-eight_hundred', '/moduledir', {})
20
20
  expect(m.name).to eq 'eight_hundred'
21
21
  expect(m.owner).to eq 'branan'
22
22
  end
23
23
 
24
24
  it "raises an error when the title is not correctly formatted" do
25
25
  expect {
26
- described_class.new('branan!eight_hundred', '/moduledir', [])
26
+ described_class.new('branan!eight_hundred', '/moduledir', {})
27
27
  }.to raise_error(ArgumentError, "Module name (branan!eight_hundred) must match either 'modulename' or 'owner/modulename'")
28
28
  end
29
29
  end
30
30
 
31
+ describe 'deleting the spec dir' do
32
+ let(:module_org) { "coolorg" }
33
+ let(:module_name) { "coolmod" }
34
+ let(:title) { "#{module_org}-#{module_name}" }
35
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
36
+ let(:spec_path) { dirname + module_name + 'spec' }
37
+
38
+ before(:each) do
39
+ logger = double("logger")
40
+ allow_any_instance_of(described_class).to receive(:logger).and_return(logger)
41
+ allow(logger).to receive(:debug2).with(any_args)
42
+ allow(logger).to receive(:info).with(any_args)
43
+ end
44
+
45
+ it 'does not remove the spec directory by default' do
46
+ FileUtils.mkdir_p(spec_path)
47
+ m = described_class.new(title, dirname, {})
48
+ m.maybe_delete_spec_dir
49
+ expect(Dir.exist?(spec_path)).to eq true
50
+ end
51
+
52
+ it 'detects a symlink and deletes the target' do
53
+ Dir.mkdir(dirname + module_name)
54
+ target_dir = Dir.mktmpdir
55
+ FileUtils.ln_s(target_dir, spec_path)
56
+ m = described_class.new(title, dirname, {exclude_spec: true})
57
+ m.maybe_delete_spec_dir
58
+ expect(Dir.exist?(target_dir)).to eq false
59
+ end
60
+
61
+ it 'removes the spec directory if exclude_spec is set' do
62
+ FileUtils.mkdir_p(spec_path)
63
+ m = described_class.new(title, dirname, {exclude_spec: true})
64
+ m.maybe_delete_spec_dir
65
+ expect(Dir.exist?(spec_path)).to eq false
66
+ end
67
+
68
+ it 'does not remove the spec directory if spec_deletable is false' do
69
+ FileUtils.mkdir_p(spec_path)
70
+ m = described_class.new(title, dirname, {})
71
+ m.spec_deletable = false
72
+ m.maybe_delete_spec_dir
73
+ expect(Dir.exist?(spec_path)).to eq true
74
+ end
75
+ end
76
+
31
77
  describe "path variables" do
32
78
  it "uses the module name as the name" do
33
- m = described_class.new('eight_hundred', '/moduledir', [])
79
+ m = described_class.new('eight_hundred', '/moduledir', {})
34
80
  expect(m.dirname).to eq '/moduledir'
35
81
  expect(m.path).to eq(Pathname.new('/moduledir/eight_hundred'))
36
82
  end
37
83
 
38
84
  it "does not include the owner in the path" do
39
- m = described_class.new('branan/eight_hundred', '/moduledir', [])
85
+ m = described_class.new('branan/eight_hundred', '/moduledir', {})
40
86
  expect(m.dirname).to eq '/moduledir'
41
87
  expect(m.path).to eq(Pathname.new('/moduledir/eight_hundred'))
42
88
  end
@@ -44,7 +90,7 @@ describe R10K::Module::Base do
44
90
 
45
91
  describe "with alternate variable names" do
46
92
  subject do
47
- described_class.new('branan/eight_hundred', '/moduledir', [])
93
+ described_class.new('branan/eight_hundred', '/moduledir', {})
48
94
  end
49
95
 
50
96
  it "aliases full_name to title" do
@@ -61,7 +107,7 @@ describe R10K::Module::Base do
61
107
  end
62
108
 
63
109
  describe "accepting a visitor" do
64
- subject { described_class.new('branan-eight_hundred', '/moduledir', []) }
110
+ subject { described_class.new('branan-eight_hundred', '/moduledir', {}) }
65
111
 
66
112
  it "passes itself to the visitor" do
67
113
  visitor = spy('visitor')
@@ -9,6 +9,28 @@ describe R10K::Module::Forge do
9
9
  let(:fixture_modulepath) { File.expand_path('spec/fixtures/module/forge', PROJECT_ROOT) }
10
10
  let(:empty_modulepath) { File.expand_path('spec/fixtures/empty', PROJECT_ROOT) }
11
11
 
12
+ describe "statically determined version support" do
13
+ it 'returns explicitly released forge versions' do
14
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { version: '8.0.0' })
15
+ expect(static_version).to eq('8.0.0')
16
+ end
17
+
18
+ it 'returns explicit pre-released forge versions' do
19
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { version: '8.0.0-pre1' })
20
+ expect(static_version).to eq('8.0.0-pre1')
21
+ end
22
+
23
+ it 'retuns nil for latest versions' do
24
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { version: :latest })
25
+ expect(static_version).to eq(nil)
26
+ end
27
+
28
+ it 'retuns nil for undefined versions' do
29
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { version: nil })
30
+ expect(static_version).to eq(nil)
31
+ end
32
+ end
33
+
12
34
  describe "implementing the Puppetfile spec" do
13
35
  it "should implement 'branan/eight_hundred', '8.0.0'" do
14
36
  expect(described_class).to be_implement('branan/eight_hundred', { version: '8.0.0' })
@@ -78,6 +100,7 @@ describe R10K::Module::Forge do
78
100
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
79
101
 
80
102
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
103
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
81
104
  expect(logger_dbl).to receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
82
105
 
83
106
  subject.sync
@@ -90,6 +113,7 @@ describe R10K::Module::Forge do
90
113
  allow_any_instance_of(described_class).to receive(:logger).and_return(logger_dbl)
91
114
 
92
115
  allow(logger_dbl).to receive(:info).with(/Deploying module to.*/)
116
+ allow(logger_dbl).to receive(:debug2).with(/No spec dir detected/)
93
117
  expect(logger_dbl).to_not receive(:warn).with(/puppet forge module.*puppetlabs-corosync.*has been deprecated/i)
94
118
 
95
119
  subject.sync
@@ -165,31 +189,54 @@ describe R10K::Module::Forge do
165
189
  describe "#sync" do
166
190
  subject { described_class.new('branan/eight_hundred', fixture_modulepath, { version: '8.0.0' }) }
167
191
 
192
+ context "syncing the repo" do
193
+ let(:module_org) { "coolorg" }
194
+ let(:module_name) { "coolmod" }
195
+ let(:title) { "#{module_org}-#{module_name}" }
196
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
197
+ let(:spec_path) { dirname + module_name + 'spec' }
198
+ subject { described_class.new(title, dirname, {}) }
199
+
200
+ it 'defaults to keeping the spec dir' do
201
+ FileUtils.mkdir_p(spec_path)
202
+ expect(subject).to receive(:status).and_return(:absent)
203
+ expect(subject).to receive(:install)
204
+ subject.sync
205
+ expect(Dir.exist?(spec_path)).to eq true
206
+ end
207
+ end
208
+
168
209
  it 'does nothing when the module is in sync' do
169
210
  allow(subject).to receive(:status).and_return :insync
170
211
 
171
212
  expect(subject).to receive(:install).never
172
213
  expect(subject).to receive(:upgrade).never
173
214
  expect(subject).to receive(:reinstall).never
174
- subject.sync
215
+ expect(subject.sync).to be false
175
216
  end
176
217
 
177
218
  it 'reinstalls the module when it is mismatched' do
178
219
  allow(subject).to receive(:status).and_return :mismatched
179
220
  expect(subject).to receive(:reinstall)
180
- subject.sync
221
+ expect(subject.sync).to be true
181
222
  end
182
223
 
183
224
  it 'upgrades the module when it is outdated' do
184
225
  allow(subject).to receive(:status).and_return :outdated
185
226
  expect(subject).to receive(:upgrade)
186
- subject.sync
227
+ expect(subject.sync).to be true
187
228
  end
188
229
 
189
230
  it 'installs the module when it is absent' do
190
231
  allow(subject).to receive(:status).and_return :absent
191
232
  expect(subject).to receive(:install)
192
- subject.sync
233
+ expect(subject.sync).to be true
234
+ end
235
+
236
+ it 'returns false if `should_sync?` is false' do
237
+ # modules do not sync if they are not requested
238
+ mod = described_class.new('my_org/my_mod', '/path/to/mod', { overrides: { modules: { requested_modules: ['other_mod'] } } })
239
+ expect(mod.sync).to be false
193
240
  end
194
241
  end
195
242
 
@@ -10,15 +10,41 @@ describe R10K::Module::Git do
10
10
  allow(R10K::Git::StatefulRepository).to receive(:new).and_return(mock_repo)
11
11
  end
12
12
 
13
+
14
+ describe "statically determined version support" do
15
+ it 'returns a given commit' do
16
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { git: 'my/remote', commit: '123adf' })
17
+ expect(static_version).to eq('123adf')
18
+ end
19
+
20
+ it 'returns a given tag' do
21
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { git: 'my/remote', tag: 'v1.2.3' })
22
+ expect(static_version).to eq('v1.2.3')
23
+ end
24
+
25
+ it 'returns a ref if it looks like a full commit sha' do
26
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { git: 'my/remote', ref: '1234567890abcdef1234567890abcdef12345678' })
27
+ expect(static_version).to eq('1234567890abcdef1234567890abcdef12345678')
28
+ end
29
+
30
+ it 'returns nil for any non-sha-like ref' do
31
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { git: 'my/remote', ref: 'refs/heads/main' })
32
+ expect(static_version).to eq(nil)
33
+ end
34
+
35
+ it 'returns nil for branches' do
36
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { git: 'my/remote', branch: 'main' })
37
+ expect(static_version).to eq(nil)
38
+ end
39
+ end
40
+
13
41
  describe "setting the owner and name" do
14
42
  describe "with a title of 'branan/eight_hundred'" do
15
43
  subject do
16
44
  described_class.new(
17
45
  'branan/eight_hundred',
18
46
  '/moduledir',
19
- {
20
- :git => 'git://git-server.site/branan/puppet-eight_hundred',
21
- }
47
+ { :git => 'git://git-server.site/branan/puppet-eight_hundred' }
22
48
  )
23
49
  end
24
50
 
@@ -40,9 +66,7 @@ describe R10K::Module::Git do
40
66
  described_class.new(
41
67
  'eight_hundred',
42
68
  '/moduledir',
43
- {
44
- :git => 'git://git-server.site/branan/puppet-eight_hundred',
45
- }
69
+ { :git => 'git://git-server.site/branan/puppet-eight_hundred' }
46
70
  )
47
71
  end
48
72
 
@@ -89,14 +113,48 @@ describe R10K::Module::Git do
89
113
  end
90
114
  end
91
115
 
116
+ describe 'syncing the repo' do
117
+ let(:module_org) { "coolorg" }
118
+ let(:module_name) { "coolmod" }
119
+ let(:title) { "#{module_org}-#{module_name}" }
120
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
121
+ let(:spec_path) { dirname + module_name + 'spec' }
122
+ subject { described_class.new(title, dirname, {}) }
123
+
124
+ before(:each) do
125
+ allow(mock_repo).to receive(:resolve).with('master').and_return('abc123')
126
+ end
127
+
128
+ it 'defaults to keeping the spec dir' do
129
+ FileUtils.mkdir_p(spec_path)
130
+ allow(mock_repo).to receive(:sync)
131
+ subject.sync
132
+ expect(Dir.exist?(spec_path)).to eq true
133
+ end
134
+
135
+ it 'returns true if repo was updated' do
136
+ expect(mock_repo).to receive(:sync).and_return(true)
137
+ expect(subject.sync).to be true
138
+ end
139
+
140
+ it 'returns false if repo was not updated (in-sync)' do
141
+ expect(mock_repo).to receive(:sync).and_return(false)
142
+ expect(subject.sync).to be false
143
+ end
144
+
145
+ it 'returns false if `should_sync?` is false' do
146
+ # modules do not sync if they are not requested
147
+ mod = described_class.new(title, dirname, { overrides: { modules: { requested_modules: ['other_mod'] } } })
148
+ expect(mod.sync).to be false
149
+ end
150
+ end
151
+
92
152
  describe "determining the status" do
93
153
  subject do
94
154
  described_class.new(
95
155
  'boolean',
96
156
  '/moduledir',
97
- {
98
- :git => 'git://git.example.com/adrienthebo/puppet-boolean'
99
- }
157
+ { :git => 'git://git.example.com/adrienthebo/puppet-boolean' }
100
158
  )
101
159
  end
102
160
 
@@ -6,6 +6,13 @@ describe R10K::Module::SVN do
6
6
 
7
7
  include_context 'fail on execution'
8
8
 
9
+ describe "statically determined version support" do
10
+ it 'is unsupported by svn backed modules' do
11
+ static_version = described_class.statically_defined_version('branan/eight_hundred', { svn: 'my/remote', revision: '123adf' })
12
+ expect(static_version).to eq(nil)
13
+ end
14
+ end
15
+
9
16
  describe "determining it implements a Puppetfile mod" do
10
17
  it "implements mods with the :svn hash key" do
11
18
  implements = described_class.implement?('r10k-fixture-repo', :svn => 'https://github.com/adrienthebo/r10k-fixture-repo')
@@ -119,6 +126,23 @@ describe R10K::Module::SVN do
119
126
  end
120
127
  end
121
128
 
129
+ describe 'the default spec dir' do
130
+ let(:module_org) { "coolorg" }
131
+ let(:module_name) { "coolmod" }
132
+ let(:title) { "#{module_org}-#{module_name}" }
133
+ let(:dirname) { Pathname.new(Dir.mktmpdir) }
134
+ let(:spec_path) { dirname + module_name + 'spec' }
135
+ subject { described_class.new(title, dirname, {}) }
136
+
137
+ it 'is kept by default' do
138
+ FileUtils.mkdir_p(spec_path)
139
+ expect(subject).to receive(:status).and_return(:absent)
140
+ expect(subject).to receive(:install).and_return(nil)
141
+ subject.sync
142
+ expect(Dir.exist?(spec_path)).to eq true
143
+ end
144
+ end
145
+
122
146
  describe "synchronizing" do
123
147
 
124
148
  subject { described_class.new('foo', '/moduledir', :svn => 'https://github.com/adrienthebo/r10k-fixture-repo', :rev => 123) }
@@ -132,7 +156,7 @@ describe R10K::Module::SVN do
132
156
 
133
157
  it "installs the SVN module" do
134
158
  expect(subject).to receive(:install)
135
- subject.sync
159
+ expect(subject.sync).to be true
136
160
  end
137
161
  end
138
162
 
@@ -142,14 +166,14 @@ describe R10K::Module::SVN do
142
166
  it "reinstalls the module" do
143
167
  expect(subject).to receive(:reinstall)
144
168
 
145
- subject.sync
169
+ expect(subject.sync).to be true
146
170
  end
147
171
 
148
172
  it "removes the existing directory" do
149
173
  expect(subject.path).to receive(:rmtree)
150
174
  allow(subject).to receive(:install)
151
175
 
152
- subject.sync
176
+ expect(subject.sync).to be true
153
177
  end
154
178
  end
155
179
 
@@ -159,7 +183,7 @@ describe R10K::Module::SVN do
159
183
  it "upgrades the repository" do
160
184
  expect(subject).to receive(:update)
161
185
 
162
- subject.sync
186
+ expect(subject.sync).to be true
163
187
  end
164
188
  end
165
189
 
@@ -171,8 +195,14 @@ describe R10K::Module::SVN do
171
195
  expect(subject).to receive(:reinstall).never
172
196
  expect(subject).to receive(:update).never
173
197
 
174
- subject.sync
198
+ expect(subject.sync).to be false
175
199
  end
176
200
  end
201
+
202
+ it 'and `should_sync?` is false' do
203
+ # modules do not sync if they are not requested
204
+ mod = described_class.new('my_mod', '/path/to/mod', { overrides: { modules: { requested_modules: ['other_mod'] } } })
205
+ expect(mod.sync).to be false
206
+ end
177
207
  end
178
208
  end