chef-dk 0.5.0.rc.1 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +63 -24
- data/lib/chef-dk/builtin_commands.rb +2 -0
- data/lib/chef-dk/command/diff.rb +312 -0
- data/lib/chef-dk/command/push.rb +1 -1
- data/lib/chef-dk/command/shell_init.rb +21 -3
- data/lib/chef-dk/command/update.rb +28 -5
- data/lib/chef-dk/configurable.rb +1 -1
- data/lib/chef-dk/exceptions.rb +3 -0
- data/lib/chef-dk/pager.rb +106 -0
- data/lib/chef-dk/policyfile/chef_repo_cookbook_source.rb +114 -0
- data/lib/chef-dk/policyfile/comparison_base.rb +124 -0
- data/lib/chef-dk/policyfile/cookbook_sources.rb +1 -0
- data/lib/chef-dk/policyfile/differ.rb +266 -0
- data/lib/chef-dk/policyfile/dsl.rb +26 -3
- data/lib/chef-dk/policyfile/uploader.rb +4 -5
- data/lib/chef-dk/policyfile_compiler.rb +8 -0
- data/lib/chef-dk/policyfile_lock.rb +135 -3
- data/lib/chef-dk/policyfile_services/install.rb +1 -0
- data/lib/chef-dk/policyfile_services/update_attributes.rb +104 -0
- data/lib/chef-dk/service_exceptions.rb +12 -0
- data/lib/chef-dk/ui.rb +8 -0
- data/lib/chef-dk/version.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/test_helpers.rb +4 -0
- data/spec/unit/command/diff_spec.rb +283 -0
- data/spec/unit/command/shell_init_spec.rb +19 -2
- data/spec/unit/command/update_spec.rb +96 -0
- data/spec/unit/command/verify_spec.rb +0 -6
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/Berksfile +3 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/README.md +4 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/chefignore +96 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/metadata.rb +9 -0
- data/spec/unit/fixtures/local_path_cookbooks/cookbook-with-a-dep/recipes/default.rb +8 -0
- data/spec/unit/pager_spec.rb +119 -0
- data/spec/unit/policyfile/chef_repo_cookbook_source_spec.rb +66 -0
- data/spec/unit/policyfile/comparison_base_spec.rb +343 -0
- data/spec/unit/policyfile/differ_spec.rb +687 -0
- data/spec/unit/policyfile_evaluation_spec.rb +87 -0
- data/spec/unit/policyfile_lock_build_spec.rb +247 -8
- data/spec/unit/policyfile_lock_serialization_spec.rb +47 -0
- data/spec/unit/policyfile_services/export_repo_spec.rb +2 -0
- data/spec/unit/policyfile_services/push_spec.rb +2 -0
- data/spec/unit/policyfile_services/update_attributes_spec.rb +217 -0
- metadata +62 -6
@@ -0,0 +1,104 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2015 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/helpers'
|
19
|
+
require 'chef-dk/policyfile/storage_config'
|
20
|
+
require 'chef-dk/service_exceptions'
|
21
|
+
require 'chef-dk/policyfile_compiler'
|
22
|
+
|
23
|
+
module ChefDK
|
24
|
+
module PolicyfileServices
|
25
|
+
|
26
|
+
class UpdateAttributes
|
27
|
+
|
28
|
+
include Policyfile::StorageConfigDelegation
|
29
|
+
include ChefDK::Helpers
|
30
|
+
|
31
|
+
attr_reader :ui
|
32
|
+
attr_reader :storage_config
|
33
|
+
|
34
|
+
def initialize(policyfile: nil, ui: nil, root_dir: nil)
|
35
|
+
@ui = ui
|
36
|
+
|
37
|
+
policyfile_rel_path = policyfile || "Policyfile.rb"
|
38
|
+
policyfile_full_path = File.expand_path(policyfile_rel_path, root_dir)
|
39
|
+
@storage_config = Policyfile::StorageConfig.new.use_policyfile(policyfile_full_path)
|
40
|
+
@updated = false
|
41
|
+
end
|
42
|
+
|
43
|
+
def run
|
44
|
+
assert_policy_and_lock_present!
|
45
|
+
|
46
|
+
if policyfile_compiler.default_attributes != policyfile_lock.default_attributes
|
47
|
+
policyfile_lock.default_attributes = policyfile_compiler.default_attributes
|
48
|
+
@updated = true
|
49
|
+
end
|
50
|
+
|
51
|
+
if policyfile_compiler.override_attributes != policyfile_lock.override_attributes
|
52
|
+
policyfile_lock.override_attributes = policyfile_compiler.override_attributes
|
53
|
+
@updated = true
|
54
|
+
end
|
55
|
+
|
56
|
+
if updated_lock?
|
57
|
+
with_file(policyfile_lock_expanded_path) do |f|
|
58
|
+
f.print(FFI_Yajl::Encoder.encode(policyfile_lock.to_lock, pretty: true ))
|
59
|
+
end
|
60
|
+
ui.msg("Updated attributes in #{policyfile_lock_expanded_path}")
|
61
|
+
else
|
62
|
+
ui.msg("Attributes already up to date")
|
63
|
+
end
|
64
|
+
rescue => error
|
65
|
+
raise PolicyfileUpdateError.new("Failed to update Policyfile lock", error)
|
66
|
+
end
|
67
|
+
|
68
|
+
def updated_lock?
|
69
|
+
@updated
|
70
|
+
end
|
71
|
+
|
72
|
+
def policyfile_content
|
73
|
+
@policyfile_content ||= IO.read(policyfile_expanded_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
def policyfile_compiler
|
77
|
+
@policyfile_compiler ||= ChefDK::PolicyfileCompiler.evaluate(policyfile_content, policyfile_expanded_path, ui: ui)
|
78
|
+
end
|
79
|
+
|
80
|
+
def policyfile_lock_content
|
81
|
+
@policyfile_lock_content ||= IO.read(policyfile_lock_expanded_path)
|
82
|
+
end
|
83
|
+
|
84
|
+
def policyfile_lock
|
85
|
+
@policyfile_lock ||= begin
|
86
|
+
lock_data = FFI_Yajl::Parser.new.parse(policyfile_lock_content)
|
87
|
+
PolicyfileLock.new(storage_config, ui: ui).build_from_lock_data(lock_data)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def assert_policy_and_lock_present!
|
93
|
+
unless File.exist?(policyfile_expanded_path)
|
94
|
+
raise PolicyfileNotFound, "Policyfile not found at path #{policyfile_expanded_path}"
|
95
|
+
end
|
96
|
+
unless File.exist?(policyfile_lock_expanded_path)
|
97
|
+
raise LockfileNotFound, "Policyfile lock not found at path #{policyfile_lock_expanded_path}"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
@@ -36,6 +36,12 @@ module ChefDK
|
|
36
36
|
class LockfileNotFound < PolicyfileServiceError
|
37
37
|
end
|
38
38
|
|
39
|
+
class MalformedLockfile < PolicyfileServiceError
|
40
|
+
end
|
41
|
+
|
42
|
+
class GitError < PolicyfileServiceError
|
43
|
+
end
|
44
|
+
|
39
45
|
class ExportDirNotEmpty < PolicyfileServiceError
|
40
46
|
end
|
41
47
|
|
@@ -70,9 +76,15 @@ module ChefDK
|
|
70
76
|
|
71
77
|
end
|
72
78
|
|
79
|
+
class PolicyfileDownloadError < PolicyfileNestedException
|
80
|
+
end
|
81
|
+
|
73
82
|
class PolicyfileInstallError < PolicyfileNestedException
|
74
83
|
end
|
75
84
|
|
85
|
+
class PolicyfileUpdateError < PolicyfileNestedException
|
86
|
+
end
|
87
|
+
|
76
88
|
class PolicyfilePushError < PolicyfileNestedException
|
77
89
|
end
|
78
90
|
|
data/lib/chef-dk/ui.rb
CHANGED
@@ -24,6 +24,10 @@ module ChefDK
|
|
24
24
|
nil
|
25
25
|
end
|
26
26
|
|
27
|
+
def print(*anything)
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
27
31
|
end
|
28
32
|
|
29
33
|
def self.null
|
@@ -45,6 +49,10 @@ module ChefDK
|
|
45
49
|
def msg(message)
|
46
50
|
@out_stream.puts(message)
|
47
51
|
end
|
52
|
+
|
53
|
+
def print(message)
|
54
|
+
@out_stream.print(message)
|
55
|
+
end
|
48
56
|
end
|
49
57
|
end
|
50
58
|
|
data/lib/chef-dk/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -23,6 +23,8 @@ require 'test_helpers'
|
|
23
23
|
require 'chef/workstation_config_loader'
|
24
24
|
|
25
25
|
RSpec.configure do |c|
|
26
|
+
running_on_windows = (RUBY_PLATFORM =~ /mswin|mingw|windows/)
|
27
|
+
|
26
28
|
c.include ChefDK
|
27
29
|
c.include TestHelpers
|
28
30
|
|
@@ -35,8 +37,12 @@ RSpec.configure do |c|
|
|
35
37
|
|
36
38
|
c.filter_run :focus => true
|
37
39
|
c.run_all_when_everything_filtered = true
|
40
|
+
# Tests that randomly fail, but may have value.
|
41
|
+
c.filter_run_excluding :volatile => true
|
42
|
+
c.filter_run_excluding :skip_on_windows => true if running_on_windows
|
38
43
|
|
39
44
|
c.mock_with(:rspec) do |mocks|
|
40
45
|
mocks.verify_partial_doubles = true
|
41
46
|
end
|
47
|
+
|
42
48
|
end
|
data/spec/test_helpers.rb
CHANGED
@@ -0,0 +1,283 @@
|
|
1
|
+
#
|
2
|
+
# Copyright:: Copyright (c) 2015 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 'shared/command_with_ui_object'
|
20
|
+
require 'chef-dk/command/diff'
|
21
|
+
|
22
|
+
describe ChefDK::Command::Diff do
|
23
|
+
|
24
|
+
it_behaves_like "a command with a UI object"
|
25
|
+
|
26
|
+
let(:params) { [] }
|
27
|
+
|
28
|
+
let(:command) do
|
29
|
+
described_class.new
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:chef_config_loader) { instance_double("Chef::WorkstationConfigLoader") }
|
33
|
+
|
34
|
+
let(:chef_config) { double("Chef::Config") }
|
35
|
+
|
36
|
+
let(:config_arg) { nil }
|
37
|
+
|
38
|
+
before do
|
39
|
+
stub_const("Chef::Config", chef_config)
|
40
|
+
allow(Chef::WorkstationConfigLoader).to receive(:new).with(config_arg).and_return(chef_config_loader)
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "selecting comparison bases" do
|
44
|
+
|
45
|
+
let(:ui) { TestHelpers::TestUI.new }
|
46
|
+
|
47
|
+
let(:http_client) { instance_double("ChefDK::AuthenticatedHTTP") }
|
48
|
+
|
49
|
+
let(:differ) { instance_double("ChefDK::Policyfile::Differ", run_report: nil) }
|
50
|
+
|
51
|
+
let(:pager) { instance_double("ChefDK::Pager", ui: ui) }
|
52
|
+
|
53
|
+
before do
|
54
|
+
allow(ChefDK::Pager).to receive(:new).and_return(pager)
|
55
|
+
allow(pager).to receive(:with_pager).and_yield(pager)
|
56
|
+
allow(command).to receive(:materialize_locks).and_return(nil)
|
57
|
+
allow(command).to receive(:differ).and_return(differ)
|
58
|
+
allow(command).to receive(:http_client).and_return(http_client)
|
59
|
+
command.ui = ui
|
60
|
+
end
|
61
|
+
|
62
|
+
context "when no base is given" do
|
63
|
+
|
64
|
+
it "prints an error message and exits" do
|
65
|
+
expect(command.run(params)).to eq(1)
|
66
|
+
expect(ui.output).to include("No comparison specified")
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when server and git comparison bases are mixed" do
|
72
|
+
|
73
|
+
let(:params) { %w{ --git gitref policygroup } }
|
74
|
+
|
75
|
+
it "prints an error message and exits" do
|
76
|
+
expect(command.run(params)).to eq(1)
|
77
|
+
expect(ui.output).to include("Conflicting arguments and options: git and Policy Group comparisons cannot be mixed")
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when specific git comparison bases are mixed with --head" do
|
83
|
+
|
84
|
+
let(:params) { %w{ --head --git gitref } }
|
85
|
+
|
86
|
+
it "prints an error message and exits" do
|
87
|
+
expect(command.run(params)).to eq(1)
|
88
|
+
expect(ui.output).to include("Conflicting git options: --head and --git are exclusive")
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "selecting git comparison bases" do
|
94
|
+
|
95
|
+
context "when the Policyfile isn't named" do
|
96
|
+
|
97
|
+
let(:params) { %w{ --head } }
|
98
|
+
|
99
|
+
it "uses Policyfile.lock.json as the local lock" do
|
100
|
+
expect(command.run(params)).to eq(0)
|
101
|
+
expect(command.policyfile_lock_relpath).to eq("Policyfile.lock.json")
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when the Policyfile is named" do
|
107
|
+
|
108
|
+
context "using the --head option" do
|
109
|
+
|
110
|
+
let(:params) { %w{ policies/OtherPolicy.rb --head } }
|
111
|
+
|
112
|
+
it "uses the corresponding lock as the local lock" do
|
113
|
+
expect(command.run(params)).to eq(0)
|
114
|
+
expect(command.policyfile_lock_relpath).to eq("policies/OtherPolicy.lock.json")
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
context "using the --git option" do
|
120
|
+
|
121
|
+
let(:params) { %w{ policies/OtherPolicy.rb --git master } }
|
122
|
+
|
123
|
+
it "uses the corresponding lock as the local lock" do
|
124
|
+
expect(command.run(params)).to eq(0)
|
125
|
+
expect(command.policyfile_lock_relpath).to eq("policies/OtherPolicy.lock.json")
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
context "when given a single commit-ish" do
|
133
|
+
|
134
|
+
let(:params) { %w{ --git master } }
|
135
|
+
|
136
|
+
it "compares the local lock to the commit" do
|
137
|
+
expect(command.run(params)).to eq(0)
|
138
|
+
expect(command.old_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Git)
|
139
|
+
expect(command.old_base.ref).to eq("master")
|
140
|
+
expect(command.new_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Local)
|
141
|
+
expect(command.new_base.policyfile_lock_relpath).to eq("Policyfile.lock.json")
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
context "when given two commit-ish names" do
|
147
|
+
|
148
|
+
let(:params) { %w{ --git master...dev-branch } }
|
149
|
+
|
150
|
+
it "compares the two commits" do
|
151
|
+
expect(command.run(params)).to eq(0)
|
152
|
+
expect(command.old_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Git)
|
153
|
+
expect(command.old_base.ref).to eq("master")
|
154
|
+
expect(command.new_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Git)
|
155
|
+
expect(command.new_base.ref).to eq("dev-branch")
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
context "when given too many commit-ish names" do
|
161
|
+
|
162
|
+
let(:params) { %w{ --git too...many...things } }
|
163
|
+
|
164
|
+
it "prints an error and exits" do
|
165
|
+
expect(command.run(params)).to eq(1)
|
166
|
+
expect(ui.output).to include("Unable to parse git comparison `too...many...things`. Only 2 references can be specified.")
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
context "when --head is used" do
|
172
|
+
|
173
|
+
let(:params) { %w{ --head } }
|
174
|
+
|
175
|
+
it "compares the local lock to git HEAD" do
|
176
|
+
expect(command.run(params)).to eq(0)
|
177
|
+
expect(command.old_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Git)
|
178
|
+
expect(command.old_base.ref).to eq("HEAD")
|
179
|
+
expect(command.new_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Local)
|
180
|
+
expect(command.new_base.policyfile_lock_relpath).to eq("Policyfile.lock.json")
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "selecting policy group comparison bases" do
|
188
|
+
|
189
|
+
let(:local_lock_comparison_base) do
|
190
|
+
instance_double("ChefDK::Policyfile::ComparisonBase::Local")
|
191
|
+
end
|
192
|
+
|
193
|
+
before do
|
194
|
+
allow(command).to receive(:local_lock_comparison_base).and_return(local_lock_comparison_base)
|
195
|
+
end
|
196
|
+
|
197
|
+
context "when the local lockfile cannot be read and parsed" do
|
198
|
+
|
199
|
+
let(:params) { %w{ dev-group } }
|
200
|
+
|
201
|
+
before do
|
202
|
+
allow(local_lock_comparison_base).to receive(:lock).and_raise(ChefDK::LockfileNotFound)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "prints an error and exits" do
|
206
|
+
expect(command.run(params)).to eq(1)
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
context "when the local lockfile can be read and parsed" do
|
212
|
+
before do
|
213
|
+
allow(local_lock_comparison_base).to receive(:lock).and_return({"name" => "example-policy"})
|
214
|
+
allow(command).to receive(:differ).and_return(differ)
|
215
|
+
command.ui = ui
|
216
|
+
end
|
217
|
+
|
218
|
+
context "when the Policyfile isn't named" do
|
219
|
+
|
220
|
+
let(:params) { %w{ dev-group } }
|
221
|
+
|
222
|
+
it "uses Policyfile.lock.json as the local lock" do
|
223
|
+
expect(command.run(params)).to eq(0)
|
224
|
+
expect(command.policyfile_lock_relpath).to eq("Policyfile.lock.json")
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
context "when the Policyfile is named" do
|
230
|
+
|
231
|
+
let(:params) { %w{ policies/SomePolicy.rb dev-group } }
|
232
|
+
|
233
|
+
it "uses the corresponding lock as the local lock" do
|
234
|
+
expect(command.run(params)).to eq(0)
|
235
|
+
expect(command.policyfile_lock_relpath).to eq("policies/SomePolicy.lock.json")
|
236
|
+
end
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
context "when given a single policy group name" do
|
241
|
+
|
242
|
+
let(:params) { %w{ dev-group } }
|
243
|
+
|
244
|
+
it "compares the policy group's lock to the local lock" do
|
245
|
+
expect(command.run(params)).to eq(0)
|
246
|
+
expect(command.old_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::PolicyGroup)
|
247
|
+
expect(command.old_base.group).to eq("dev-group")
|
248
|
+
expect(command.new_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::Local)
|
249
|
+
expect(command.new_base.policyfile_lock_relpath).to eq("Policyfile.lock.json")
|
250
|
+
end
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
context "when given two policy group names" do
|
255
|
+
|
256
|
+
let(:params) { %w{ prod-group...stage-group } }
|
257
|
+
|
258
|
+
it "compares the two locks" do
|
259
|
+
expect(command.run(params)).to eq(0)
|
260
|
+
expect(command.old_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::PolicyGroup)
|
261
|
+
expect(command.old_base.group).to eq("prod-group")
|
262
|
+
expect(command.new_base).to be_a_kind_of(ChefDK::Policyfile::ComparisonBase::PolicyGroup)
|
263
|
+
expect(command.new_base.group).to eq("stage-group")
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
context "when given too many policy group names" do
|
269
|
+
|
270
|
+
let(:params) { %w{ prod...stage...dev } }
|
271
|
+
|
272
|
+
it "prints an error and exits" do
|
273
|
+
expect(command.run(params)).to eq(1)
|
274
|
+
expect(ui.output).to include("Unable to parse policy group comparison `prod...stage...dev`. Only 2 references can be specified.")
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|