chef-dk 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/chef-dk/authenticated_http.rb +40 -0
- data/lib/chef-dk/chef_runner.rb +5 -0
- data/lib/chef-dk/command/exec.rb +4 -1
- data/lib/chef-dk/command/generate.rb +11 -0
- data/lib/chef-dk/command/generator_commands.rb +20 -365
- data/lib/chef-dk/command/generator_commands/app.rb +99 -0
- data/lib/chef-dk/command/generator_commands/attribute.rb +37 -0
- data/lib/chef-dk/command/generator_commands/base.rb +76 -0
- data/lib/chef-dk/command/generator_commands/cookbook.rb +100 -0
- data/lib/chef-dk/command/generator_commands/cookbook_code_file.rb +99 -0
- data/lib/chef-dk/command/generator_commands/cookbook_file.rb +45 -0
- data/lib/chef-dk/command/generator_commands/lwrp.rb +36 -0
- data/lib/chef-dk/command/generator_commands/recipe.rb +36 -0
- data/lib/chef-dk/command/generator_commands/repo.rb +96 -0
- data/lib/chef-dk/command/generator_commands/template.rb +45 -0
- data/lib/chef-dk/command/verify.rb +28 -0
- data/lib/chef-dk/component_test.rb +16 -3
- data/lib/chef-dk/cookbook_omnifetch.rb +2 -0
- data/lib/chef-dk/cookbook_profiler/identifiers.rb +3 -15
- data/lib/chef-dk/exceptions.rb +15 -0
- data/lib/chef-dk/generator.rb +102 -25
- data/lib/chef-dk/policyfile/community_cookbook_source.rb +0 -7
- data/lib/chef-dk/policyfile/{cookbook_spec.rb → cookbook_location_specification.rb} +35 -6
- data/lib/chef-dk/policyfile/cookbook_locks.rb +305 -0
- data/lib/chef-dk/policyfile/dsl.rb +26 -12
- data/lib/chef-dk/policyfile/read_cookbook_for_compat_mode_upload.rb +70 -0
- data/lib/chef-dk/policyfile/solution_dependencies.rb +204 -0
- data/lib/chef-dk/policyfile/storage_config.rb +77 -0
- data/lib/chef-dk/policyfile/uploader.rb +110 -0
- data/lib/chef-dk/policyfile_compiler.rb +59 -29
- data/lib/chef-dk/policyfile_lock.rb +104 -160
- data/lib/chef-dk/skeletons/code_generator/files/default/Berksfile +1 -1
- data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +0 -1
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/README.md +66 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/Rakefile +65 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/certificates/README.md +19 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/README-policy.md +9 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/README.md +54 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/data_bags/README.md +63 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/environments/README.md +5 -0
- data/lib/chef-dk/skeletons/code_generator/files/default/repo/roles/README.md +16 -0
- data/lib/chef-dk/skeletons/code_generator/recipes/cookbook.rb +7 -1
- data/lib/chef-dk/skeletons/code_generator/recipes/repo.rb +62 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/LICENSE.all_rights.erb +3 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/LICENSE.apache2.erb +201 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/LICENSE.gplv2.erb +339 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/LICENSE.gplv3.erb +674 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/LICENSE.mit.erb +21 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/default_recipe.rb.erb +1 -4
- data/lib/chef-dk/skeletons/code_generator/templates/default/metadata.rb.erb +3 -3
- data/lib/chef-dk/skeletons/code_generator/templates/default/repo/config/rake.rb.erb +38 -0
- data/lib/chef-dk/skeletons/code_generator/templates/default/repo/gitignore.erb +11 -0
- data/lib/chef-dk/version.rb +1 -1
- data/spec/shared/a_file_generator.rb +121 -0
- data/spec/shared/a_generated_file.rb +12 -0
- data/spec/shared/fixture_cookbook_checksums.rb +47 -0
- data/spec/spec_helper.rb +4 -2
- data/spec/unit/chef_runner_spec.rb +12 -5
- data/spec/unit/cli_spec.rb +4 -4
- data/spec/unit/command/base_spec.rb +1 -1
- data/spec/unit/command/exec_spec.rb +37 -27
- data/spec/unit/command/generate_spec.rb +3 -3
- data/spec/unit/command/generator_commands/app_spec.rb +131 -0
- data/spec/unit/command/generator_commands/attribute_spec.rb +32 -0
- data/spec/unit/command/generator_commands/cookbook_file_spec.rb +32 -0
- data/spec/unit/command/generator_commands/cookbook_spec.rb +205 -0
- data/spec/unit/command/generator_commands/lwrp_spec.rb +32 -0
- data/spec/unit/command/generator_commands/recipe_spec.rb +32 -0
- data/spec/unit/command/generator_commands/repo_spec.rb +287 -0
- data/spec/unit/command/generator_commands/template_spec.rb +32 -0
- data/spec/unit/command/shell_init_spec.rb +4 -4
- data/spec/unit/command/verify_spec.rb +9 -9
- data/spec/unit/commands_map_spec.rb +1 -1
- data/spec/unit/component_test_spec.rb +3 -3
- data/spec/unit/cookbook_profiler/git_spec.rb +7 -7
- data/spec/unit/cookbook_profiler/identifiers_spec.rb +12 -8
- data/spec/unit/fixtures/cookbook_cache/baz-f59ee7a5bca6a4e606b67f7f856b768d847c39bb/Berksfile +1 -1
- data/spec/unit/fixtures/cookbook_cache/dep_of_bar-1.2.3/Berksfile +1 -1
- data/spec/unit/fixtures/cookbook_cache/foo-1.0.0/Berksfile +1 -1
- data/spec/unit/fixtures/example_cookbook/Berksfile +1 -1
- data/spec/unit/fixtures/local_path_cookbooks/another-local-cookbook/README.md +4 -0
- data/spec/unit/fixtures/local_path_cookbooks/another-local-cookbook/chefignore +96 -0
- data/spec/unit/fixtures/local_path_cookbooks/another-local-cookbook/metadata.rb +8 -0
- data/spec/unit/fixtures/local_path_cookbooks/another-local-cookbook/recipes/default.rb +8 -0
- data/spec/unit/fixtures/local_path_cookbooks/local-cookbook/Berksfile +1 -1
- data/spec/unit/fixtures/local_path_cookbooks/noignore-f59ee7a5bca6a4e606b67f7f856b768d847c39bb/.kitchen.yml +16 -0
- data/spec/unit/fixtures/local_path_cookbooks/noignore-f59ee7a5bca6a4e606b67f7f856b768d847c39bb/README.md +4 -0
- data/spec/unit/fixtures/local_path_cookbooks/noignore-f59ee7a5bca6a4e606b67f7f856b768d847c39bb/metadata.rb +8 -0
- data/spec/unit/fixtures/local_path_cookbooks/noignore-f59ee7a5bca6a4e606b67f7f856b768d847c39bb/recipes/default.rb +8 -0
- data/spec/unit/generator_spec.rb +120 -0
- data/spec/unit/policyfile/{cookbook_spec_spec.rb → cookbook_location_specification_spec.rb} +83 -38
- data/spec/unit/policyfile/cookbook_locks_spec.rb +354 -0
- data/spec/unit/policyfile/read_cookbook_for_compat_mode_upload_spec.rb +85 -0
- data/spec/unit/policyfile/solution_dependencies_spec.rb +145 -0
- data/spec/unit/policyfile/storage_config_spec.rb +98 -0
- data/spec/unit/policyfile/uploader_spec.rb +292 -0
- data/spec/unit/policyfile_demands_spec.rb +177 -24
- data/spec/unit/policyfile_evaluation_spec.rb +40 -12
- data/spec/unit/{policyfile_builder_spec.rb → policyfile_lock_build_spec.rb} +179 -64
- data/spec/unit/policyfile_lock_install_spec.rb +138 -0
- data/spec/unit/policyfile_lock_validation_spec.rb +610 -0
- metadata +103 -59
- data/spec/unit/command/generator_commands_spec.rb +0 -504
@@ -0,0 +1,98 @@
|
|
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/policyfile/storage_config'
|
20
|
+
|
21
|
+
describe ChefDK::Policyfile::StorageConfig do
|
22
|
+
|
23
|
+
let(:config_options) { {} }
|
24
|
+
|
25
|
+
let(:storage_config) do
|
26
|
+
described_class.new(config_options)
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with explicit path options" do
|
30
|
+
|
31
|
+
let(:cache_path) do
|
32
|
+
File.expand_path("spec/unit/fixtures/cookbook_cache", project_root)
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:relative_paths_root) do
|
36
|
+
File.expand_path("spec/unit/fixtures/", project_root)
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:config_options) do
|
40
|
+
{ cache_path: cache_path, relative_paths_root: relative_paths_root }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "uses the provided option for relative_paths_root" do
|
44
|
+
expect(storage_config.relative_paths_root).to eq(relative_paths_root)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "uses the provided cache_path" do
|
48
|
+
expect(storage_config.cache_path).to eq(cache_path)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with default options" do
|
53
|
+
|
54
|
+
it "defaults to the CookbookOmnifetch configured cache path" do
|
55
|
+
expect(storage_config.cache_path).to eq(CookbookOmnifetch.storage_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "defaults to the current working directory for relative_paths_root" do
|
59
|
+
expect(storage_config.relative_paths_root).to eq(Dir.pwd)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "updating storage config for policyfile location" do
|
64
|
+
|
65
|
+
before do
|
66
|
+
storage_config.use_policyfile("/path/to/Policyfile.rb")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "updates the relative_paths_root to be relative to a policyfile" do
|
70
|
+
expect(storage_config.relative_paths_root).to eq("/path/to")
|
71
|
+
end
|
72
|
+
|
73
|
+
it "stores the location of the policyfile" do
|
74
|
+
expect(storage_config.policyfile_filename).to eq("/path/to/Policyfile.rb")
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
describe "updating storage config for policyfile lock location" do
|
81
|
+
|
82
|
+
before do
|
83
|
+
storage_config.use_policyfile_lock("/path/to/Policyfile.lock.json")
|
84
|
+
end
|
85
|
+
|
86
|
+
it "updates the relative_paths_root to be relative to a policyfile" do
|
87
|
+
expect(storage_config.relative_paths_root).to eq("/path/to")
|
88
|
+
end
|
89
|
+
|
90
|
+
it "stores the location of the policyfile" do
|
91
|
+
expect(storage_config.policyfile_lock_filename).to eq("/path/to/Policyfile.lock.json")
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
end
|
98
|
+
|
@@ -0,0 +1,292 @@
|
|
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/policyfile/uploader'
|
20
|
+
|
21
|
+
# We load this here to ensure we get the "verifying doubles" behavior from
|
22
|
+
# RSpec. It's not used by Policyfile::Uploader, but it's a collaborator.
|
23
|
+
require 'chef-dk/authenticated_http'
|
24
|
+
|
25
|
+
describe ChefDK::Policyfile::Uploader do
|
26
|
+
|
27
|
+
let(:policyfile_lock_data) do
|
28
|
+
{
|
29
|
+
"name"=> "example",
|
30
|
+
"run_list"=> [ "recipe[omnibus::default]" ],
|
31
|
+
"cookbook_locks"=> {
|
32
|
+
"omnibus"=> {
|
33
|
+
"version"=> "2.2.0",
|
34
|
+
"identifier"=> "64b3e64306cff223206348e46af545b19032b170",
|
35
|
+
"dotted_decimal_identifier"=> "28345299219435506.9887234981653237.76628930769264",
|
36
|
+
"cache_key"=> "omnibus-2cf98f9797cacce9c8688fc4e74858b858e2bc14",
|
37
|
+
"origin"=> "git@github.com:opscode-cookbooks/omnibus.git",
|
38
|
+
"source_options"=> {
|
39
|
+
"git"=> "git@github.com:opscode-cookbooks/omnibus.git",
|
40
|
+
"revision"=> "2cf98f9797cacce9c8688fc4e74858b858e2bc14",
|
41
|
+
"branch"=> "master"
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
let(:policyfile_lock) { instance_double("ChefDK::PolicyfileLock", name: "example",
|
49
|
+
to_lock: policyfile_lock_data) }
|
50
|
+
|
51
|
+
let(:policy_group) { "unit-test" }
|
52
|
+
|
53
|
+
let(:http_client) { instance_double("ChefDK::AuthenticatedHTTP") }
|
54
|
+
|
55
|
+
let(:uploader) { described_class.new(policyfile_lock, policy_group, http_client: http_client) }
|
56
|
+
|
57
|
+
let(:policyfile_as_data_bag_item) do
|
58
|
+
|
59
|
+
policyfile_as_data_bag_item = {
|
60
|
+
"id" => "example-unit-test",
|
61
|
+
"name" => "data_bag_item_policyfiles_example-unit-test",
|
62
|
+
"data_bag" => "policyfiles"
|
63
|
+
}
|
64
|
+
policyfile_as_data_bag_item["raw_data"] = policyfile_lock_data.dup
|
65
|
+
policyfile_as_data_bag_item["raw_data"]["id"] = "example-unit-test"
|
66
|
+
policyfile_as_data_bag_item["json_class"] = "Chef::DataBagItem"
|
67
|
+
policyfile_as_data_bag_item
|
68
|
+
end
|
69
|
+
|
70
|
+
it "has a lockfile" do
|
71
|
+
expect(uploader.policyfile_lock).to eq(policyfile_lock)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "has a policy group" do
|
75
|
+
expect(uploader.policy_group).to eq(policy_group)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "has an HTTP client" do
|
79
|
+
expect(uploader.http_client).to eq(http_client)
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when created without an HTTP client" do
|
83
|
+
|
84
|
+
let(:http_client) { nil }
|
85
|
+
|
86
|
+
it "creates an HTTP client with default config" do
|
87
|
+
skip "TODO: determine correct behavior"
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
describe "creating uploading documents in compat mode" do
|
94
|
+
|
95
|
+
let(:cookbook_locks) { {} }
|
96
|
+
let(:cookbook_versions) { {} }
|
97
|
+
|
98
|
+
let(:existing_cookbook_on_remote) do
|
99
|
+
{"apt"=>
|
100
|
+
{"url"=>"http://localhost:8889/cookbooks/apt",
|
101
|
+
"versions"=>
|
102
|
+
[{"url"=>
|
103
|
+
"http://localhost:8889/cookbooks/apt/46097674477573307.43471642740453733.243606720748315",
|
104
|
+
"version"=>"46097674477573307.43471642740453733.243606720748315"}]},
|
105
|
+
"build-essential"=>
|
106
|
+
{"url"=>"http://localhost:8889/cookbooks/build-essential",
|
107
|
+
"versions"=>
|
108
|
+
[{"url"=>
|
109
|
+
"http://localhost:8889/cookbooks/build-essential/67369247788170534.26353953100055918.55660493423796",
|
110
|
+
"version"=>"67369247788170534.26353953100055918.55660493423796"}]},
|
111
|
+
"java"=>
|
112
|
+
{"url"=>"http://localhost:8889/cookbooks/java",
|
113
|
+
"versions"=>
|
114
|
+
[{"url"=>
|
115
|
+
"http://localhost:8889/cookbooks/java/5664982062912610.52588194571203830.6215746262253",
|
116
|
+
"version"=>"5664982062912610.52588194571203830.6215746262253"}]},
|
117
|
+
"jenkins"=>
|
118
|
+
{"url"=>"http://localhost:8889/cookbooks/jenkins",
|
119
|
+
"versions"=>
|
120
|
+
[{"url"=>
|
121
|
+
"http://localhost:8889/cookbooks/jenkins/69194928762630300.30177357398946006.269829039948647",
|
122
|
+
"version"=>"69194928762630300.30177357398946006.269829039948647"}]}
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
before do
|
127
|
+
allow(policyfile_lock).to receive(:cookbook_locks).and_return(cookbook_locks)
|
128
|
+
end
|
129
|
+
|
130
|
+
def lock_double(name, dotted_decimal_id)
|
131
|
+
cache_path = "/home/user/cache_path/#{name}"
|
132
|
+
|
133
|
+
lock = instance_double("ChefDK::Policyfile::CookbookLock",
|
134
|
+
name: name,
|
135
|
+
dotted_decimal_identifier: dotted_decimal_id,
|
136
|
+
cookbook_path: cache_path)
|
137
|
+
|
138
|
+
cookbook_version = instance_double("Chef::CookbookVersion",
|
139
|
+
name: name,
|
140
|
+
version: dotted_decimal_id)
|
141
|
+
|
142
|
+
allow(ChefDK::Policyfile::ReadCookbookForCompatModeUpload).
|
143
|
+
to receive(:load).
|
144
|
+
with(name, dotted_decimal_id, cache_path).
|
145
|
+
and_return(cookbook_version)
|
146
|
+
|
147
|
+
cookbook_versions[name] = cookbook_version
|
148
|
+
cookbook_locks[name] = lock
|
149
|
+
|
150
|
+
lock
|
151
|
+
end
|
152
|
+
|
153
|
+
it "ensures a data bag named 'policyfiles' exists" do
|
154
|
+
expect(http_client).to receive(:post).with('data', {"name" => "policyfiles"})
|
155
|
+
uploader.data_bag_create
|
156
|
+
end
|
157
|
+
|
158
|
+
it "does not error when the 'policyfiles' data bag exists" do
|
159
|
+
response = double("Net::HTTP response", code: "409")
|
160
|
+
error = Net::HTTPServerException.new("conflict", response)
|
161
|
+
expect(http_client).to receive(:post).with('data', {"name" => "policyfiles"}).and_raise(error)
|
162
|
+
expect { uploader.data_bag_create }.to_not raise_error
|
163
|
+
end
|
164
|
+
|
165
|
+
it "uploads the policyfile as a data bag item" do
|
166
|
+
response = double("Net::HTTP response", code: "404")
|
167
|
+
error = Net::HTTPServerException.new("Not Found", response)
|
168
|
+
expect(http_client).to receive(:put).
|
169
|
+
with('data/policyfiles/example-unit-test', policyfile_as_data_bag_item).
|
170
|
+
and_raise(error)
|
171
|
+
expect(http_client).to receive(:post).
|
172
|
+
with('data/policyfiles', policyfile_as_data_bag_item)
|
173
|
+
|
174
|
+
uploader.data_bag_item_create
|
175
|
+
end
|
176
|
+
|
177
|
+
it "replaces an existing policyfile on the server if it exists" do
|
178
|
+
expect(http_client).to receive(:put).
|
179
|
+
with('data/policyfiles/example-unit-test', policyfile_as_data_bag_item)
|
180
|
+
|
181
|
+
uploader.data_bag_item_create
|
182
|
+
end
|
183
|
+
|
184
|
+
it "enumerates the cookbooks already on the server" do
|
185
|
+
expect(http_client).to receive(:get).with('cookbooks?num_versions=all').and_return(existing_cookbook_on_remote)
|
186
|
+
expect(uploader.existing_cookbook_on_remote).to eq(existing_cookbook_on_remote)
|
187
|
+
end
|
188
|
+
|
189
|
+
context "with an empty policyfile lock" do
|
190
|
+
|
191
|
+
it "has an empty list of cookbooks for possible upload" do
|
192
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
193
|
+
|
194
|
+
expect(uploader.cookbook_versions_for_policy).to eq([])
|
195
|
+
end
|
196
|
+
|
197
|
+
it "has an empty list of cookbooks that need to be uploaded" do
|
198
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
199
|
+
|
200
|
+
expect(uploader.cookbook_versions_to_upload).to eq([])
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
|
205
|
+
context "with a set of cookbooks that don't exist on the server" do
|
206
|
+
|
207
|
+
before do
|
208
|
+
lock_double("my_apache2", "123.456.789")
|
209
|
+
lock_double("my_jenkins", "321.654.987")
|
210
|
+
end
|
211
|
+
|
212
|
+
it "lists the cookbooks in the lock as possibly needing to be uploaded" do
|
213
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
214
|
+
|
215
|
+
expect(uploader.cookbook_versions_for_policy).to eq(cookbook_versions.values)
|
216
|
+
end
|
217
|
+
|
218
|
+
it "lists all cookbooks in the lock as needing to be uploaded" do
|
219
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
220
|
+
expect(http_client).to receive(:get).with('cookbooks?num_versions=all').and_return(existing_cookbook_on_remote)
|
221
|
+
|
222
|
+
expect(uploader.cookbook_versions_to_upload).to eq(cookbook_versions.values)
|
223
|
+
end
|
224
|
+
|
225
|
+
it "uploads the cookbooks and then the policy" do
|
226
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
227
|
+
expect(http_client).to receive(:get).with('cookbooks?num_versions=all').and_return(existing_cookbook_on_remote)
|
228
|
+
|
229
|
+
cookbook_uploader = instance_double("Chef::CookbookUploader")
|
230
|
+
expect(Chef::CookbookUploader).to receive(:new).
|
231
|
+
with(cookbook_versions.values, :rest => http_client).
|
232
|
+
and_return(cookbook_uploader)
|
233
|
+
expect(cookbook_uploader).to receive(:upload_cookbooks)
|
234
|
+
|
235
|
+
# behavior for these tested above
|
236
|
+
expect(uploader).to receive(:data_bag_create)
|
237
|
+
expect(uploader).to receive(:data_bag_item_create)
|
238
|
+
|
239
|
+
uploader.upload
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
context "with a set of cookbooks where some already exist on the server" do
|
245
|
+
|
246
|
+
before do
|
247
|
+
# These are new:
|
248
|
+
lock_double("my_apache2", "123.456.789")
|
249
|
+
lock_double("my_jenkins", "321.654.987")
|
250
|
+
|
251
|
+
# Have this one:
|
252
|
+
lock_double("build-essential", "67369247788170534.26353953100055918.55660493423796")
|
253
|
+
end
|
254
|
+
|
255
|
+
let(:expected_cookbooks_for_upload) do
|
256
|
+
[
|
257
|
+
cookbook_versions["my_apache2"],
|
258
|
+
cookbook_versions["my_jenkins"]
|
259
|
+
]
|
260
|
+
end
|
261
|
+
|
262
|
+
it "lists only cookbooks not on the server as needing to be uploaded" do
|
263
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
264
|
+
expect(http_client).to receive(:get).with('cookbooks?num_versions=all').and_return(existing_cookbook_on_remote)
|
265
|
+
|
266
|
+
|
267
|
+
expect(uploader.cookbook_versions_to_upload).to eq(expected_cookbooks_for_upload)
|
268
|
+
end
|
269
|
+
|
270
|
+
it "uploads the cookbooks and then the policy" do
|
271
|
+
expect(policyfile_lock).to receive(:validate_cookbooks!)
|
272
|
+
expect(http_client).to receive(:get).with('cookbooks?num_versions=all').and_return(existing_cookbook_on_remote)
|
273
|
+
|
274
|
+
cookbook_uploader = instance_double("Chef::CookbookUploader")
|
275
|
+
expect(Chef::CookbookUploader).to receive(:new).
|
276
|
+
with(expected_cookbooks_for_upload, :rest => http_client).
|
277
|
+
and_return(cookbook_uploader)
|
278
|
+
expect(cookbook_uploader).to receive(:upload_cookbooks)
|
279
|
+
|
280
|
+
# behavior for these tested above
|
281
|
+
expect(uploader).to receive(:data_bag_create)
|
282
|
+
expect(uploader).to receive(:data_bag_item_create)
|
283
|
+
|
284
|
+
uploader.upload
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
292
|
+
|
@@ -29,8 +29,6 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
29
29
|
let(:policyfile) do
|
30
30
|
policyfile = ChefDK::PolicyfileCompiler.new.build do |p|
|
31
31
|
|
32
|
-
p.policyfile_filename = "/no-such-place/Policyfile.rb"
|
33
|
-
|
34
32
|
p.default_source(*default_source) if default_source
|
35
33
|
p.run_list(*run_list)
|
36
34
|
|
@@ -128,6 +126,29 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
128
126
|
end
|
129
127
|
end
|
130
128
|
|
129
|
+
describe "when normalizing run_list items" do
|
130
|
+
|
131
|
+
it "normalizes a bare cookbook name" do
|
132
|
+
policyfile.run_list("local-cookbook")
|
133
|
+
expect(policyfile.normalized_run_list).to eq(["recipe[local-cookbook::default]"])
|
134
|
+
end
|
135
|
+
|
136
|
+
it "normalizes a bare cookbook::recipe item" do
|
137
|
+
policyfile.run_list("local-cookbook::server")
|
138
|
+
expect(policyfile.normalized_run_list).to eq(["recipe[local-cookbook::server]"])
|
139
|
+
end
|
140
|
+
|
141
|
+
it "normalizes a recipe[] item with implicit default" do
|
142
|
+
policyfile.run_list("recipe[local-cookbook]")
|
143
|
+
expect(policyfile.normalized_run_list).to eq(["recipe[local-cookbook::default]"])
|
144
|
+
end
|
145
|
+
|
146
|
+
it "does not modify a fully qualified recipe" do
|
147
|
+
policyfile.run_list("recipe[local-cookbook::jazz_hands]")
|
148
|
+
expect(policyfile.normalized_run_list).to eq(["recipe[local-cookbook::jazz_hands]"])
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
131
152
|
|
132
153
|
before do
|
133
154
|
expect(policyfile.errors).to eq([])
|
@@ -149,6 +170,14 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
149
170
|
expect(policyfile).to receive(:ensure_cache_dir_exists)
|
150
171
|
expect(policyfile.graph_solution).to eq({})
|
151
172
|
end
|
173
|
+
|
174
|
+
it "has an empty set of solution_dependencies" do
|
175
|
+
expected_solution_deps = {
|
176
|
+
"Policyfile" => [],
|
177
|
+
"dependencies" => {}
|
178
|
+
}
|
179
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
180
|
+
end
|
152
181
|
end
|
153
182
|
|
154
183
|
context "Given a run list and no local or git cookbooks" do
|
@@ -172,6 +201,14 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
172
201
|
expect(policyfile.graph_solution).to eq({"remote-cb" => "1.1.1"})
|
173
202
|
end
|
174
203
|
|
204
|
+
it "includes the cookbook in the solution dependencies" do
|
205
|
+
expected_solution_deps = {
|
206
|
+
"Policyfile" => [],
|
207
|
+
"dependencies" => { "remote-cb (1.1.1)" => [] }
|
208
|
+
}
|
209
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
210
|
+
end
|
211
|
+
|
175
212
|
end
|
176
213
|
|
177
214
|
context "And the default source is the chef-server" do
|
@@ -199,8 +236,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
199
236
|
|
200
237
|
before do
|
201
238
|
policyfile.dsl.cookbook('local-cookbook', path: "/foo")
|
202
|
-
policyfile.
|
203
|
-
policyfile.
|
239
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:version).and_return("2.3.4")
|
240
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:dependencies).and_return([])
|
241
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:ensure_cached).and_return(true)
|
204
242
|
end
|
205
243
|
|
206
244
|
it "demands a solution using the local cookbook" do
|
@@ -214,6 +252,14 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
214
252
|
expect(policyfile.artifacts_graph).to eq(expected_artifacts_graph)
|
215
253
|
end
|
216
254
|
|
255
|
+
it "includes the cookbook in the solution dependencies" do
|
256
|
+
expected_solution_deps = {
|
257
|
+
"Policyfile" => [ [ "local-cookbook", ">= 0.0.0" ] ],
|
258
|
+
"dependencies" => { "local-cookbook (2.3.4)" => [] }
|
259
|
+
}
|
260
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
261
|
+
end
|
262
|
+
|
217
263
|
end
|
218
264
|
|
219
265
|
context "Given a local cookbook with a dependency and only the local cookbook in the run list" do
|
@@ -226,9 +272,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
226
272
|
|
227
273
|
before do
|
228
274
|
policyfile.dsl.cookbook("local-cookbook", path: "foo/")
|
229
|
-
policyfile.
|
230
|
-
policyfile.
|
231
|
-
policyfile.
|
275
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:ensure_cached)
|
276
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:version).and_return("2.3.4")
|
277
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:dependencies).and_return([ [ "local-cookbook-dep-one", "~> 1.0"] ])
|
232
278
|
end
|
233
279
|
|
234
280
|
it "demands a solution using the local cookbook" do
|
@@ -248,6 +294,18 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
248
294
|
expect(policyfile.graph_solution).to eq({"local-cookbook" => "2.3.4", "local-cookbook-dep-one" => "1.5.0"})
|
249
295
|
end
|
250
296
|
|
297
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
298
|
+
expected_solution_deps = {
|
299
|
+
"Policyfile" => [ [ "local-cookbook", ">= 0.0.0" ] ],
|
300
|
+
"dependencies" => {
|
301
|
+
"local-cookbook (2.3.4)" => [[ "local-cookbook-dep-one", "~> 1.0"]],
|
302
|
+
"local-cookbook-dep-one (1.5.0)" => []
|
303
|
+
}
|
304
|
+
|
305
|
+
}
|
306
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
307
|
+
end
|
308
|
+
|
251
309
|
end
|
252
310
|
context "And the default source is the chef server" do
|
253
311
|
|
@@ -255,9 +313,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
255
313
|
|
256
314
|
before do
|
257
315
|
policyfile.dsl.cookbook("local-cookbook", path: "foo/")
|
258
|
-
policyfile.
|
259
|
-
policyfile.
|
260
|
-
policyfile.
|
316
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:ensure_cached)
|
317
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:version).and_return("2.3.4")
|
318
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:dependencies).and_return([ [ "local-cookbook-dep-one", "~> 1.0"] ])
|
261
319
|
end
|
262
320
|
|
263
321
|
it "demands a solution using the local cookbook" do
|
@@ -280,6 +338,18 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
280
338
|
expect(policyfile.graph_solution).to eq({"local-cookbook" => "2.3.4", "local-cookbook-dep-one" => "1.6.0"})
|
281
339
|
end
|
282
340
|
|
341
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
342
|
+
expected_solution_deps = {
|
343
|
+
"Policyfile" => [ [ "local-cookbook", ">= 0.0.0" ] ],
|
344
|
+
"dependencies" => {
|
345
|
+
"local-cookbook (2.3.4)" => [[ "local-cookbook-dep-one", "~> 1.0"]],
|
346
|
+
"local-cookbook-dep-one (1.6.0)" => []
|
347
|
+
}
|
348
|
+
|
349
|
+
}
|
350
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
351
|
+
end
|
352
|
+
|
283
353
|
end
|
284
354
|
end
|
285
355
|
|
@@ -289,9 +359,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
289
359
|
|
290
360
|
before do
|
291
361
|
policyfile.dsl.cookbook("git-sourced-cookbook", git: "git://git.example.org:user/a-cookbook.git")
|
292
|
-
policyfile.
|
293
|
-
policyfile.
|
294
|
-
policyfile.
|
362
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:ensure_cached)
|
363
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:version).and_return("8.6.7")
|
364
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:dependencies).and_return([ ])
|
295
365
|
end
|
296
366
|
|
297
367
|
it "demands a solution using the git sourced cookbook" do
|
@@ -309,6 +379,18 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
309
379
|
expect(policyfile).to receive(:ensure_cache_dir_exists)
|
310
380
|
expect(policyfile.graph_solution).to eq({"git-sourced-cookbook" => "8.6.7"})
|
311
381
|
end
|
382
|
+
|
383
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
384
|
+
expected_solution_deps = {
|
385
|
+
"Policyfile" => [ [ "git-sourced-cookbook", ">= 0.0.0" ] ],
|
386
|
+
"dependencies" => {
|
387
|
+
"git-sourced-cookbook (8.6.7)" => []
|
388
|
+
}
|
389
|
+
|
390
|
+
}
|
391
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
392
|
+
end
|
393
|
+
|
312
394
|
end
|
313
395
|
|
314
396
|
context "Given a git-sourced cookbook with a dependency and only the git cookbook in the run list" do
|
@@ -317,9 +399,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
317
399
|
|
318
400
|
before do
|
319
401
|
policyfile.dsl.cookbook("git-sourced-cookbook", git: "git://git.example.org:user/a-cookbook.git")
|
320
|
-
policyfile.
|
321
|
-
policyfile.
|
322
|
-
policyfile.
|
402
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:ensure_cached)
|
403
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:version).and_return("8.6.7")
|
404
|
+
allow(policyfile.cookbook_location_spec_for("git-sourced-cookbook")).to receive(:dependencies).and_return([ ["git-sourced-cookbook-dep", "~> 2.2" ] ])
|
323
405
|
end
|
324
406
|
|
325
407
|
context "And the default source is the community site" do
|
@@ -342,6 +424,19 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
342
424
|
expect(policyfile).to receive(:ensure_cache_dir_exists)
|
343
425
|
expect(policyfile.graph_solution).to eq({"git-sourced-cookbook" => "8.6.7", "git-sourced-cookbook-dep" => "2.8.0"})
|
344
426
|
end
|
427
|
+
|
428
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
429
|
+
expected_solution_deps = {
|
430
|
+
"Policyfile" => [ [ "git-sourced-cookbook", ">= 0.0.0" ] ],
|
431
|
+
"dependencies" => {
|
432
|
+
"git-sourced-cookbook (8.6.7)" => [ [ "git-sourced-cookbook-dep", "~> 2.2" ] ],
|
433
|
+
"git-sourced-cookbook-dep (2.8.0)" => []
|
434
|
+
}
|
435
|
+
|
436
|
+
}
|
437
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
438
|
+
end
|
439
|
+
|
345
440
|
end
|
346
441
|
|
347
442
|
context "And the default source is the chef server" do
|
@@ -364,6 +459,19 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
364
459
|
expect(policyfile).to receive(:ensure_cache_dir_exists)
|
365
460
|
expect(policyfile.graph_solution).to eq({"git-sourced-cookbook" => "8.6.7", "git-sourced-cookbook-dep" => "2.9.0"})
|
366
461
|
end
|
462
|
+
|
463
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
464
|
+
expected_solution_deps = {
|
465
|
+
"Policyfile" => [ [ "git-sourced-cookbook", ">= 0.0.0" ] ],
|
466
|
+
"dependencies" => {
|
467
|
+
"git-sourced-cookbook (8.6.7)" => [ [ "git-sourced-cookbook-dep", "~> 2.2" ] ],
|
468
|
+
"git-sourced-cookbook-dep (2.9.0)" => []
|
469
|
+
}
|
470
|
+
|
471
|
+
}
|
472
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
473
|
+
end
|
474
|
+
|
367
475
|
end
|
368
476
|
end
|
369
477
|
|
@@ -373,9 +481,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
373
481
|
|
374
482
|
before do
|
375
483
|
policyfile.dsl.cookbook("local-cookbook", path: "foo/")
|
376
|
-
policyfile.
|
377
|
-
policyfile.
|
378
|
-
policyfile.
|
484
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:ensure_cached)
|
485
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:version).and_return("2.3.4")
|
486
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:dependencies).and_return([])
|
379
487
|
end
|
380
488
|
|
381
489
|
context "And the default source is the community site" do
|
@@ -397,6 +505,18 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
397
505
|
expect(policyfile.graph_solution).to eq({"local-cookbook" => "2.3.4", "remote-cb" => "1.1.1"})
|
398
506
|
end
|
399
507
|
|
508
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
509
|
+
expected_solution_deps = {
|
510
|
+
"Policyfile" => [ [ "local-cookbook", ">= 0.0.0" ] ],
|
511
|
+
"dependencies" => {
|
512
|
+
"local-cookbook (2.3.4)" => [],
|
513
|
+
"remote-cb (1.1.1)" => []
|
514
|
+
}
|
515
|
+
|
516
|
+
}
|
517
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
518
|
+
end
|
519
|
+
|
400
520
|
end
|
401
521
|
|
402
522
|
context "And the default source is the chef server" do
|
@@ -418,6 +538,18 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
418
538
|
expect(policyfile.graph_solution).to eq({"local-cookbook" => "2.3.4", "remote-cb" => "1.1.1"})
|
419
539
|
end
|
420
540
|
|
541
|
+
it "includes the cookbook and dependencies in the solution dependencies" do
|
542
|
+
expected_solution_deps = {
|
543
|
+
"Policyfile" => [ [ "local-cookbook", ">= 0.0.0" ] ],
|
544
|
+
"dependencies" => {
|
545
|
+
"local-cookbook (2.3.4)" => [],
|
546
|
+
"remote-cb (1.1.1)" => []
|
547
|
+
}
|
548
|
+
|
549
|
+
}
|
550
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
551
|
+
end
|
552
|
+
|
421
553
|
end
|
422
554
|
end
|
423
555
|
|
@@ -440,6 +572,16 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
440
572
|
expect(policyfile.graph_solution).to eq({"remote-cb" => "0.1.0"})
|
441
573
|
end
|
442
574
|
|
575
|
+
it "includes the policyfile constraint in the solution dependencies" do
|
576
|
+
expected_solution_deps = {
|
577
|
+
"Policyfile" => [ [ "remote-cb", "~> 0.1" ] ],
|
578
|
+
"dependencies" => {
|
579
|
+
"remote-cb (0.1.0)" => []
|
580
|
+
}
|
581
|
+
|
582
|
+
}
|
583
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
584
|
+
end
|
443
585
|
end
|
444
586
|
|
445
587
|
context "given a cookbook that isn't in the run list is specified with a version constraint in the policyfile" do
|
@@ -453,9 +595,9 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
453
595
|
|
454
596
|
policyfile.dsl.cookbook("local-cookbook", path: "foo/")
|
455
597
|
|
456
|
-
policyfile.
|
457
|
-
policyfile.
|
458
|
-
policyfile.
|
598
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:ensure_cached)
|
599
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:version).and_return("2.3.4")
|
600
|
+
allow(policyfile.cookbook_location_spec_for("local-cookbook")).to receive(:dependencies).and_return([])
|
459
601
|
end
|
460
602
|
|
461
603
|
it "demands a solution that matches the version constraint in the policyfile" do
|
@@ -468,16 +610,27 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
|
|
468
610
|
end
|
469
611
|
|
470
612
|
it "builds a policyfile lock from the constraints" do
|
471
|
-
|
613
|
+
skip
|
472
614
|
expect(policyfile).to receive(:cache_path).and_return(Pathname.new("~/.nopenope/cache"))
|
473
615
|
expect(policyfile.lock).to eq(:wat)
|
474
616
|
end
|
475
617
|
|
618
|
+
it "includes the policyfile constraint in the solution dependencies" do
|
619
|
+
expected_solution_deps = {
|
620
|
+
"Policyfile" => [ [ "remote-cb", "~> 0.1" ], [ "local-cookbook", ">= 0.0.0"] ],
|
621
|
+
"dependencies" => {
|
622
|
+
"local-cookbook (2.3.4)" => [],
|
623
|
+
"remote-cb (0.1.0)" => []
|
624
|
+
}
|
625
|
+
|
626
|
+
}
|
627
|
+
expect(policyfile.solution_dependencies.to_lock).to eq(expected_solution_deps)
|
628
|
+
end
|
476
629
|
end
|
477
630
|
|
478
631
|
context "Given a run_list with roles" do
|
479
632
|
it "expands the roles from the given role source" do
|
480
|
-
|
633
|
+
skip
|
481
634
|
end
|
482
635
|
end
|
483
636
|
|