chef-dk 0.2.1 → 0.3.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +2 -2
  3. data/README.md +25 -0
  4. data/lib/chef-dk/builtin_commands.rb +4 -0
  5. data/lib/chef-dk/cli.rb +46 -0
  6. data/lib/chef-dk/command/base.rb +4 -0
  7. data/lib/chef-dk/command/generator_commands/template.rb +2 -1
  8. data/lib/chef-dk/command/install.rb +105 -0
  9. data/lib/chef-dk/command/push.rb +123 -0
  10. data/lib/chef-dk/cookbook_profiler/identifiers.rb +5 -0
  11. data/lib/chef-dk/exceptions.rb +38 -0
  12. data/lib/chef-dk/generator.rb +16 -1
  13. data/lib/chef-dk/helpers.rb +1 -1
  14. data/lib/chef-dk/policyfile/cookbook_location_specification.rb +4 -0
  15. data/lib/chef-dk/policyfile/cookbook_locks.rb +73 -0
  16. data/lib/chef-dk/policyfile/reports/install.rb +70 -0
  17. data/lib/chef-dk/policyfile/reports/table_printer.rb +58 -0
  18. data/lib/chef-dk/policyfile/reports/upload.rb +70 -0
  19. data/lib/chef-dk/policyfile/solution_dependencies.rb +102 -8
  20. data/lib/chef-dk/policyfile/uploader.rb +37 -6
  21. data/lib/chef-dk/policyfile_compiler.rb +19 -5
  22. data/lib/chef-dk/policyfile_lock.rb +122 -9
  23. data/lib/chef-dk/policyfile_services/install.rb +131 -0
  24. data/lib/chef-dk/policyfile_services/push.rb +121 -0
  25. data/lib/chef-dk/skeletons/code_generator/recipes/cookbook.rb +6 -4
  26. data/lib/chef-dk/ui.rb +50 -0
  27. data/lib/chef-dk/version.rb +1 -1
  28. data/spec/shared/a_file_generator.rb +4 -1
  29. data/spec/test_helpers.rb +21 -0
  30. data/spec/unit/cli_spec.rb +100 -1
  31. data/spec/unit/command/base_spec.rb +23 -0
  32. data/spec/unit/command/exec_spec.rb +2 -2
  33. data/spec/unit/command/install_spec.rb +159 -0
  34. data/spec/unit/command/push_spec.rb +203 -0
  35. data/spec/unit/command/shell_init_spec.rb +1 -1
  36. data/spec/unit/policyfile/cookbook_location_specification_spec.rb +7 -0
  37. data/spec/unit/policyfile/cookbook_locks_spec.rb +13 -2
  38. data/spec/unit/policyfile/reports/install_spec.rb +115 -0
  39. data/spec/unit/policyfile/reports/upload_spec.rb +96 -0
  40. data/spec/unit/policyfile/solution_dependencies_spec.rb +1 -1
  41. data/spec/unit/policyfile/uploader_spec.rb +9 -12
  42. data/spec/unit/policyfile_lock_serialization_spec.rb +292 -0
  43. data/spec/unit/policyfile_services/install_spec.rb +170 -0
  44. data/spec/unit/policyfile_services/push_spec.rb +202 -0
  45. metadata +48 -6
@@ -0,0 +1,121 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'chef-dk/authenticated_http'
19
+ require 'chef-dk/policyfile_compiler'
20
+ require 'chef-dk/policyfile/uploader'
21
+
22
+ module ChefDK
23
+ module PolicyfileServices
24
+ class Push
25
+
26
+ attr_reader :root_dir
27
+ attr_reader :config
28
+ attr_reader :policy_group
29
+ attr_reader :ui
30
+
31
+ def initialize(policyfile: nil, ui: nil, policy_group: nil, config: nil, root_dir: nil)
32
+ @root_dir = root_dir
33
+ @policyfile_relative_path = policyfile
34
+ @ui = ui
35
+ @config = config
36
+ @policy_group = policy_group
37
+
38
+ @http_client = nil
39
+ @storage_config = nil
40
+ @policy_data = nil
41
+ end
42
+
43
+ def policyfile_relative_path
44
+ @policyfile_relative_path || "Policyfile.rb"
45
+ end
46
+
47
+ def policyfile_path
48
+ File.expand_path(policyfile_relative_path, root_dir)
49
+ end
50
+
51
+ def lockfile_relative_path
52
+ policyfile_relative_path.gsub(/\.rb\Z/, '') + ".lock.json"
53
+ end
54
+
55
+ def lockfile_path
56
+ File.expand_path(lockfile_relative_path, root_dir)
57
+ end
58
+
59
+ def http_client
60
+ @http_client ||= ChefDK::AuthenticatedHTTP.new(config.chef_server_url,
61
+ signing_key_filename: config.client_key,
62
+ client_name: config.node_name)
63
+ end
64
+
65
+ def policy_data
66
+ @policy_data ||= FFI_Yajl::Parser.parse(IO.read(lockfile_path))
67
+ rescue => error
68
+ raise PolicyfilePushError.new("Error reading lockfile #{lockfile_path}", error)
69
+ end
70
+
71
+ def storage_config
72
+ @storage_config ||= ChefDK::Policyfile::StorageConfig.new.use_policyfile_lock(lockfile_path)
73
+ end
74
+
75
+ def uploader
76
+ ChefDK::Policyfile::Uploader.new(policyfile_lock, policy_group, ui: ui, http_client: http_client)
77
+ end
78
+
79
+ def run
80
+ unless File.exist?(lockfile_path)
81
+ raise LockfileNotFound, "No lockfile at #{lockfile_path} - you need to run `install` before `push`"
82
+ end
83
+
84
+ validate_lockfile
85
+ write_updated_lockfile
86
+ upload_policy
87
+
88
+ end
89
+
90
+ def policyfile_lock
91
+ @policyfile_lock || validate_lockfile
92
+ end
93
+
94
+ private
95
+
96
+ def upload_policy
97
+ uploader.upload
98
+ rescue => error
99
+ raise PolicyfilePushError.new("Failed to upload policy to policy group #{policy_group}", error)
100
+ end
101
+
102
+ def write_updated_lockfile
103
+ File.open(lockfile_path, "w+") do |f|
104
+ f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
105
+ end
106
+ end
107
+
108
+ def validate_lockfile
109
+ return @policyfile_lock if @policyfile_lock
110
+ @policyfile_lock = ChefDK::PolicyfileLock.new(storage_config).build_from_lock_data(policy_data)
111
+ # TODO: enumerate any cookbook that have been updated
112
+ @policyfile_lock.validate_cookbooks!
113
+ @policyfile_lock
114
+ rescue => error
115
+ raise PolicyfilePushError.new("Invalid lockfile data", error)
116
+ end
117
+
118
+ end
119
+ end
120
+ end
121
+
@@ -43,11 +43,13 @@ template "#{cookbook_dir}/recipes/default.rb" do
43
43
  end
44
44
 
45
45
  # git
46
- if context.have_git && !context.skip_git_init
46
+ if context.have_git
47
+ if !context.skip_git_init
47
48
 
48
- execute("initialize-git") do
49
- command("git init .")
50
- cwd cookbook_dir
49
+ execute("initialize-git") do
50
+ command("git init .")
51
+ cwd cookbook_dir
52
+ end
51
53
  end
52
54
 
53
55
  cookbook_file "#{cookbook_dir}/.gitignore" do
@@ -0,0 +1,50 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module ChefDK
19
+ class UI
20
+
21
+ class NullStream
22
+
23
+ def puts(*anything)
24
+ nil
25
+ end
26
+
27
+ end
28
+
29
+ def self.null
30
+ new(out: NullStream.new, err: NullStream.new)
31
+ end
32
+
33
+ attr_reader :out_stream
34
+ attr_reader :err_stream
35
+
36
+ def initialize(out: nil, err: nil)
37
+ @out_stream = out || $stdout
38
+ @err_stream = err || $stderr
39
+ end
40
+
41
+ def err(message)
42
+ @err_stream.puts(message)
43
+ end
44
+
45
+ def msg(message)
46
+ @out_stream.puts(message)
47
+ end
48
+ end
49
+ end
50
+
@@ -16,5 +16,5 @@
16
16
  #
17
17
 
18
18
  module ChefDK
19
- VERSION = "0.2.1"
19
+ VERSION = "0.3.0"
20
20
  end
@@ -32,6 +32,10 @@ shared_examples_for "a file generator" do
32
32
  reset_tempdir
33
33
  end
34
34
 
35
+ after(:each) do
36
+ ChefDK::Generator::Context.reset
37
+ end
38
+
35
39
  context "when argv is empty" do
36
40
  let(:argv) { [] }
37
41
 
@@ -118,4 +122,3 @@ shared_examples_for "a file generator" do
118
122
  end
119
123
 
120
124
  end
121
-
@@ -56,4 +56,25 @@ module TestHelpers
56
56
  @tmpdir ||= Dir.mktmpdir("chef-dk")
57
57
  File.realpath(@tmpdir)
58
58
  end
59
+
60
+ class TestUI
61
+
62
+ attr_reader :output_stream
63
+
64
+ def initialize
65
+ @output_stream = StringIO.new
66
+ end
67
+
68
+ def err(message)
69
+ @output_stream.puts(message)
70
+ end
71
+
72
+ def msg(message)
73
+ @output_stream.puts(message)
74
+ end
75
+
76
+ def output
77
+ @output_stream.string
78
+ end
79
+ end
59
80
  end
@@ -1,4 +1,3 @@
1
- #
2
1
  # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
2
  # License:: Apache License, Version 2.0
4
3
  #
@@ -58,6 +57,12 @@ E
58
57
  let(:version_message) { "Chef Development Kit Version: #{ChefDK::VERSION}\n" }
59
58
 
60
59
  def run_cli(expected_exit_code)
60
+ expect(cli).to receive(:exit).with(expected_exit_code)
61
+ expect(cli).to receive(:sanity_check!)
62
+ cli.run
63
+ end
64
+
65
+ def run_cli_with_sanity_check(expected_exit_code)
61
66
  expect(cli).to receive(:exit).with(expected_exit_code)
62
67
  cli.run
63
68
  end
@@ -107,6 +112,18 @@ E
107
112
  end
108
113
  end
109
114
 
115
+ context "given an invalid option" do
116
+
117
+ let(:argv) { %w[-nope] }
118
+
119
+ it "prints an 'invalid option message and the help output, then exits non-zero" do
120
+ run_cli(1)
121
+ expect(stdout).to eq(base_help_message)
122
+ expect(stderr).to eq("invalid option: -nope\n")
123
+ end
124
+
125
+ end
126
+
110
127
  context "given an invalid/unknown subcommand" do
111
128
  let(:argv) { %w[ancient-aliens] }
112
129
 
@@ -148,4 +165,86 @@ E
148
165
  end
149
166
  end
150
167
 
168
+ context "sanity_check!" do
169
+ context "on unix" do
170
+ it "complains if embedded is first" do
171
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/embedded/bin:/opt/chefdk/bin' })
172
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
173
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
174
+ run_cli_with_sanity_check(0)
175
+ expect(stdout).not_to eq(base_help_message)
176
+ expect(stdout).to include("please reverse that order")
177
+ expect(stdout).to include("chef shell-init")
178
+ end
179
+
180
+ it "complains if only embedded is present" do
181
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/embedded/bin' })
182
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
183
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
184
+ run_cli_with_sanity_check(0)
185
+ expect(stdout).not_to eq(base_help_message)
186
+ expect(stdout).to include("you must add")
187
+ expect(stdout).to include("chef shell-init")
188
+ end
189
+
190
+ it "passes when both are present in the correct order" do
191
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/bin:/opt/chefdk/embedded/bin' })
192
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
193
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
194
+ run_cli_with_sanity_check(0)
195
+ expect(stdout).to eq(base_help_message)
196
+ end
197
+
198
+ it "passes when only the omnibus bin dir is present" do
199
+ expect(cli).to receive(:env).and_return({'PATH' => '/opt/chefdk/bin' })
200
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("/opt/chefdk/embedded/bin")
201
+ allow(cli).to receive(:omnibus_bin_dir).and_return("/opt/chefdk/bin")
202
+ run_cli_with_sanity_check(0)
203
+ expect(stdout).to eq(base_help_message)
204
+ end
205
+ end
206
+
207
+ context "on windows" do
208
+ before do
209
+ allow(Chef::Platform).to receive(:windows?).and_return(true)
210
+ stub_const("File::PATH_SEPARATOR", ';')
211
+ end
212
+
213
+ it "complains if embedded is first" do
214
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\embedded\bin;C:\opscode\chefdk\bin' })
215
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
216
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
217
+ run_cli_with_sanity_check(0)
218
+ expect(stdout).not_to eq(base_help_message)
219
+ expect(stdout).to include("please reverse that order")
220
+ expect(stdout).to include("chef shell-init")
221
+ end
222
+
223
+ it "complains if only embedded is present" do
224
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\embedded\bin' })
225
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
226
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
227
+ run_cli_with_sanity_check(0)
228
+ expect(stdout).not_to eq(base_help_message)
229
+ expect(stdout).to include("you must add")
230
+ expect(stdout).to include("chef shell-init")
231
+ end
232
+
233
+ it "passes when both are present in the correct order" do
234
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\bin;C:\opscode\chefdk\embedded\bin' })
235
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
236
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
237
+ run_cli_with_sanity_check(0)
238
+ expect(stdout).to eq(base_help_message)
239
+ end
240
+
241
+ it "passes when only the omnibus bin dir is present" do
242
+ expect(cli).to receive(:env).and_return({'PATH' => 'C:\opscode\chefdk\bin' })
243
+ allow(cli).to receive(:omnibus_embedded_bin_dir).and_return("c:/opscode/chefdk/embedded/bin")
244
+ allow(cli).to receive(:omnibus_bin_dir).and_return("c:/opscode/chefdk/bin")
245
+ run_cli_with_sanity_check(0)
246
+ expect(stdout).to eq(base_help_message)
247
+ end
248
+ end
249
+ end
151
250
  end
@@ -34,6 +34,7 @@ describe ChefDK::Command::Base do
34
34
  end
35
35
  end
36
36
 
37
+ let(:stderr_io) { StringIO.new }
37
38
  let(:stdout_io) { StringIO.new }
38
39
  let(:command_instance) { TestCommand.new() }
39
40
 
@@ -41,8 +42,13 @@ describe ChefDK::Command::Base do
41
42
  stdout_io.string
42
43
  end
43
44
 
45
+ def stderr
46
+ stderr_io.string
47
+ end
48
+
44
49
  before do
45
50
  allow(command_instance).to receive(:stdout).and_return(stdout_io)
51
+ allow(command_instance).to receive(:stderr).and_return(stderr_io)
46
52
  end
47
53
 
48
54
 
@@ -85,4 +91,21 @@ describe ChefDK::Command::Base do
85
91
  expect(stdout).to eq("thanks for passing me true\n")
86
92
  end
87
93
 
94
+ describe "when given invalid options" do
95
+
96
+ it "prints the help banner and exits gracefully" do
97
+ expect(run_command(%w[-foo])).to eq(1)
98
+
99
+ expect(stderr).to eq("invalid option: -foo\n")
100
+
101
+ expected = <<-E
102
+ use me please
103
+ -u, --user If the user exists
104
+
105
+ E
106
+ expect(stdout).to eq(expected)
107
+ end
108
+
109
+ end
110
+
88
111
  end
@@ -73,7 +73,7 @@ describe ChefDK::Command::Exec do
73
73
 
74
74
  let(:expected_GEM_ROOT) { Gem.default_dir.inspect }
75
75
 
76
- let(:expected_GEM_HOME) { ENV['GEM_HOME'] }
76
+ let(:expected_GEM_HOME) { Gem.user_dir }
77
77
 
78
78
  let(:expected_GEM_PATH) { Gem.path.join(':') }
79
79
 
@@ -107,7 +107,7 @@ describe ChefDK::Command::Exec do
107
107
 
108
108
  let(:expected_GEM_ROOT) { Gem.default_dir.inspect }
109
109
 
110
- let(:expected_GEM_HOME) { ENV['GEM_HOME'] }
110
+ let(:expected_GEM_HOME) { Gem.user_dir }
111
111
 
112
112
  let(:expected_GEM_PATH) { Gem.path.join(':') }
113
113
 
@@ -0,0 +1,159 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'spec_helper'
19
+ require 'chef-dk/command/install'
20
+
21
+ describe ChefDK::Command::Install do
22
+
23
+ let(:params) { [] }
24
+
25
+ let(:command) do
26
+ c = described_class.new
27
+ c.apply_params!(params)
28
+ c
29
+ end
30
+
31
+ let(:install_service) { instance_double(ChefDK::PolicyfileServices::Install) }
32
+
33
+ it "disables debug by default" do
34
+ expect(command.debug?).to be(false)
35
+ end
36
+
37
+ it "configures a default UI component" do
38
+ ui = command.ui
39
+ expect(ui.out_stream).to eq($stdout)
40
+ expect(ui.err_stream).to eq($stderr)
41
+ end
42
+
43
+ context "when debug mode is set" do
44
+
45
+ let(:params) { [ "-D" ] }
46
+
47
+ it "enables debug" do
48
+ expect(command.debug?).to be(true)
49
+ end
50
+ end
51
+
52
+ context "with no arguments" do
53
+
54
+ it "does not specify a policyfile relative path" do
55
+ expect(command.policyfile_relative_path).to be(nil)
56
+ end
57
+
58
+ it "creates the installer service with a `nil` policyfile path" do
59
+ expect(ChefDK::PolicyfileServices::Install).to receive(:new).
60
+ with(policyfile: nil, ui: command.ui, root_dir: Dir.pwd).
61
+ and_return(install_service)
62
+ expect(command.installer).to eq(install_service)
63
+ end
64
+
65
+ end
66
+
67
+ context "with an explicit policyfile relative path" do
68
+
69
+ let(:params) { [ "MyPolicy.rb" ] }
70
+
71
+ it "respects the user-supplied path" do
72
+ expect(command.policyfile_relative_path).to eq("MyPolicy.rb")
73
+ end
74
+
75
+ it "creates the installer service with the specified policyfile path" do
76
+ expect(ChefDK::PolicyfileServices::Install).to receive(:new).
77
+ with(policyfile: "MyPolicy.rb", ui: command.ui, root_dir: Dir.pwd).
78
+ and_return(install_service)
79
+ expect(command.installer).to eq(install_service)
80
+ end
81
+
82
+ end
83
+
84
+ describe "running the install" do
85
+
86
+ let(:ui) { TestHelpers::TestUI.new }
87
+
88
+ before do
89
+ command.ui = ui
90
+ allow(command).to receive(:installer).and_return(install_service)
91
+ end
92
+
93
+ context "when the command is successful" do
94
+ before do
95
+ expect(install_service).to receive(:run)
96
+ end
97
+
98
+ it "returns 0" do
99
+ expect(command.run).to eq(0)
100
+ end
101
+ end
102
+
103
+ context "when the command is unsuccessful" do
104
+
105
+ let(:backtrace) { caller[0...3] }
106
+
107
+ let(:cause) do
108
+ e = StandardError.new("some operation failed")
109
+ e.set_backtrace(backtrace)
110
+ e
111
+ end
112
+
113
+ let(:exception) do
114
+ ChefDK::PolicyfileInstallError.new("install failed", cause)
115
+ end
116
+
117
+ before do
118
+ expect(install_service).to receive(:run).and_raise(exception)
119
+ end
120
+
121
+ it "returns 1" do
122
+ expect(command.run).to eq(1)
123
+ end
124
+
125
+ it "displays the exception and cause" do
126
+ expected_error_text=<<-E
127
+ Error: install failed
128
+ Reason: StandardError
129
+
130
+ some operation failed
131
+ E
132
+
133
+ command.run
134
+ expect(ui.output).to eq(expected_error_text)
135
+ end
136
+
137
+ context "and debug is enabled" do
138
+
139
+ let(:params) { ["-D"] }
140
+
141
+ it "displays the exception and cause with backtrace" do
142
+ expected_error_text=<<-E
143
+ Error: install failed
144
+ Reason: StandardError
145
+
146
+ some operation failed
147
+ E
148
+
149
+ expected_error_text << backtrace.join("\n") << "\n"
150
+
151
+ command.run
152
+ expect(ui.output).to eq(expected_error_text)
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end