chef-dk 0.4.0 → 0.5.0.rc.1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +1 -3
  3. data/README.md +20 -15
  4. data/lib/chef-dk/cli.rb +18 -1
  5. data/lib/chef-dk/command/verify.rb +1 -1
  6. data/lib/chef-dk/policyfile/cookbook_location_specification.rb +5 -1
  7. data/lib/chef-dk/policyfile/read_cookbook_for_compat_mode_upload.rb +44 -0
  8. data/lib/chef-dk/policyfile/uploader.rb +58 -6
  9. data/lib/chef-dk/policyfile_lock.rb +42 -0
  10. data/lib/chef-dk/skeletons/code_generator/files/default/{repo/cookbooks → cookbook_readmes}/README-policy.md +0 -0
  11. data/lib/chef-dk/skeletons/code_generator/files/default/{repo/cookbooks → cookbook_readmes}/README.md +1 -1
  12. data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/example/attributes/default.rb +7 -0
  13. data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/example/metadata.rb +3 -0
  14. data/lib/chef-dk/skeletons/code_generator/files/default/repo/cookbooks/example/recipes/default.rb +8 -0
  15. data/lib/chef-dk/skeletons/code_generator/files/default/repo/data_bags/README.md +13 -18
  16. data/lib/chef-dk/skeletons/code_generator/files/default/repo/data_bags/example/example_item.json +4 -0
  17. data/lib/chef-dk/skeletons/code_generator/files/default/repo/environments/README.md +7 -3
  18. data/lib/chef-dk/skeletons/code_generator/files/default/repo/environments/example.json +13 -0
  19. data/lib/chef-dk/skeletons/code_generator/files/default/repo/roles/README.md +6 -13
  20. data/lib/chef-dk/skeletons/code_generator/files/default/repo/roles/example.json +13 -0
  21. data/lib/chef-dk/skeletons/code_generator/recipes/app.rb +5 -1
  22. data/lib/chef-dk/skeletons/code_generator/recipes/cookbook.rb +5 -1
  23. data/lib/chef-dk/skeletons/code_generator/recipes/repo.rb +5 -20
  24. data/lib/chef-dk/version.rb +1 -1
  25. data/lib/kitchen/provisioner/policyfile_zero.rb +21 -9
  26. data/spec/spec_helper.rb +8 -0
  27. data/spec/unit/cli_spec.rb +49 -3
  28. data/spec/unit/command/generator_commands/app_spec.rb +1 -1
  29. data/spec/unit/command/generator_commands/cookbook_spec.rb +1 -1
  30. data/spec/unit/command/generator_commands/repo_spec.rb +46 -50
  31. data/spec/unit/policyfile/uploader_spec.rb +225 -171
  32. data/spec/unit/policyfile_evaluation_spec.rb +16 -0
  33. data/spec/unit/policyfile_lock_build_spec.rb +156 -0
  34. metadata +18 -9
  35. data/lib/chef-dk/skeletons/code_generator/files/default/repo/Rakefile +0 -65
  36. data/lib/chef-dk/skeletons/code_generator/files/default/repo/certificates/README.md +0 -19
  37. data/lib/chef-dk/skeletons/code_generator/templates/default/repo/config/rake.rb.erb +0 -38
@@ -0,0 +1,3 @@
1
+ name 'example'
2
+ description 'An example cookbook'
3
+ version '1.0.0'
@@ -0,0 +1,8 @@
1
+ # This is a Chef recipe file. It can be used to specify resources which will
2
+ # apply configuration to a server.
3
+
4
+ log "Welcome to Chef, #{node["example"]["name"]}!" do
5
+ level :info
6
+ end
7
+
8
+ # For more information, see the documentation: http://docs.getchef.com/essentials_cookbook_recipes.html
@@ -3,28 +3,19 @@ Data Bags
3
3
 
4
4
  This directory contains directories of the various data bags you create for your infrastructure. Each subdirectory corresponds to a data bag on the Chef Server, and contains JSON files of the items that go in the bag.
5
5
 
6
- First, create a directory for the data bag.
6
+ For example, in this directory you'll find an example data bag directory called `example`, which contains an item definition called `example_item.json`
7
+
8
+ Before uploading this item to the server, we must first create the data bag on the Chef Server.
7
9
 
8
- mkdir data_bags/BAG
10
+ knife data bag create example
9
11
 
10
- Then create the JSON files for items that will go into that bag.
12
+ Then we can upload the items in the data bag's directory to the Chef Server.
11
13
 
12
- $EDITOR data_bags/BAG/ITEM.json
13
-
14
- The JSON for the ITEM must contain a key named "id" with a value equal to "ITEM". For example,
15
-
16
- {
17
- "id": "foo"
18
- }
19
-
20
- Next, create the data bag on the Chef Server.
21
-
22
- knife data bag create BAG
23
-
24
- Then upload the items in the data bag's directory to the Chef Server.
25
-
26
- knife data bag from file BAG ITEM.json
14
+ knife data bag from file example example_item.json
27
15
 
16
+ For more information on data bags, see the Chef wiki page:
17
+
18
+ https://docs.getchef.com/essentials_data_bags.html
28
19
 
29
20
  Encrypted Data Bags
30
21
  -------------------
@@ -61,3 +52,7 @@ Use the secret_key to view the contents.
61
52
  id: mysql
62
53
  password: abc123
63
54
 
55
+
56
+ For more information on encrypted data bags, see the Chef wiki page:
57
+
58
+ https://docs.getchef.com/essentials_data_bags.html
@@ -1,5 +1,9 @@
1
- Requires Chef 0.10.0+.
1
+ Create environments here, in either the Role Ruby DSL (.rb) or JSON (.json) files. To install environments on the server, use knife.
2
2
 
3
- This directory is for Ruby DSL and JSON files for environments. For more information see "About Environments" in the Chef documentation:
3
+ For example, in this directory you'll find an example environment file called `example.json` which can be uploaded to the Chef Server:
4
4
 
5
- http://docs.chef.io/environments.html
5
+ knife environment from file environments/example.json
6
+
7
+ For more information on environments, see the Chef wiki page:
8
+
9
+ https://docs.chef.io/environments.html
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "example",
3
+ "description": "This is an example environment defined as JSON",
4
+ "chef_type": "environment",
5
+ "json_class": "Chef::Environment",
6
+ "default_attributes": {
7
+ },
8
+ "override_attributes": {
9
+ },
10
+ "cookbook_versions": {
11
+ "example": "= 1.0.0"
12
+ }
13
+ }
@@ -1,16 +1,9 @@
1
1
  Create roles here, in either the Role Ruby DSL (.rb) or JSON (.json) files. To install roles on the server, use knife.
2
2
 
3
- For example, create `roles/base_example.rb`:
4
-
5
- name "base_example"
6
- description "Example base role applied to all nodes."
7
- # List of recipes and roles to apply. Requires Chef 0.8, earlier versions use 'recipes()'.
8
- #run_list()
9
- # Attributes applied if the node doesn't have it set already.
10
- #default_attributes()
11
- # Attributes applied no matter what the node has set already.
12
- #override_attributes()
13
-
14
- Then upload it to the Chef Server:
3
+ For example, in this directory you'll find an example role file called `example.json` which can be uploaded to the Chef Server:
15
4
 
16
- knife role from file roles/base_example.rb
5
+ knife role from file roles/example.json
6
+
7
+ For more information on roles, see the Chef wiki page:
8
+
9
+ https://docs.getchef.com/essentials_roles.html
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "example",
3
+ "description": "This is an example role defined as JSON",
4
+ "chef_type": "role",
5
+ "json_class": "Chef::Role",
6
+ "default_attributes": {
7
+ },
8
+ "override_attributes": {
9
+ },
10
+ "run_list": [
11
+ "recipe[example]"
12
+ ]
13
+ }
@@ -22,7 +22,11 @@ directory "#{app_dir}/test/integration/default/serverspec" do
22
22
  recursive true
23
23
  end
24
24
 
25
- cookbook_file "#{app_dir}/test/integration/default/serverspec/spec_helper.rb" do
25
+ directory "#{app_dir}/test/integration/helpers/serverspec" do
26
+ recursive true
27
+ end
28
+
29
+ cookbook_file "#{app_dir}/test/integration/helpers/serverspec/spec_helper.rb" do
26
30
  source 'serverspec_spec_helper.rb'
27
31
  action :create_if_missing
28
32
  end
@@ -36,7 +36,11 @@ directory "#{cookbook_dir}/test/integration/default/serverspec" do
36
36
  recursive true
37
37
  end
38
38
 
39
- cookbook_file "#{cookbook_dir}/test/integration/default/serverspec/spec_helper.rb" do
39
+ directory "#{cookbook_dir}/test/integration/helpers/serverspec" do
40
+ recursive true
41
+ end
42
+
43
+ cookbook_file "#{cookbook_dir}/test/integration/helpers/serverspec/spec_helper.rb" do
40
44
  source 'serverspec_spec_helper.rb'
41
45
  action :create_if_missing
42
46
  end
@@ -14,36 +14,21 @@ cookbook_file "#{repo_dir}/README.md" do
14
14
  source "repo/README.md"
15
15
  end
16
16
 
17
- cookbook_file "#{repo_dir}/Rakefile" do
18
- source "repo/Rakefile"
19
- end
20
-
21
17
  cookbook_file "#{repo_dir}/chefignore" do
22
18
  source "chefignore"
23
19
  end
24
20
 
25
- directory "#{repo_dir}/config"
26
-
27
- template "#{repo_dir}/config/rake.rb" do
28
- source "repo/config/rake.rb.erb"
29
- helpers(ChefDK::Generator::TemplateHelper)
30
- end
31
-
32
- %w{certificates data_bags environments roles}.each do |tlo|
33
- directory "#{repo_dir}/#{tlo}"
34
-
35
- cookbook_file "#{repo_dir}/#{tlo}/README.md" do
36
- source "repo/#{tlo}/README.md"
21
+ %w{cookbooks data_bags environments roles}.each do |tlo|
22
+ remote_directory "#{repo_dir}/#{tlo}" do
23
+ source "repo/#{tlo}"
37
24
  end
38
25
  end
39
26
 
40
- directory "#{repo_dir}/cookbooks"
41
-
42
27
  cookbook_file "#{repo_dir}/cookbooks/README.md" do
43
28
  if context.policy_only
44
- source "repo/cookbooks/README-policy.md"
29
+ source "cookbook_readmes/README-policy.md"
45
30
  else
46
- source "repo/cookbooks/README.md"
31
+ source "cookbook_readmes/README.md"
47
32
  end
48
33
  end
49
34
 
@@ -16,5 +16,5 @@
16
16
  #
17
17
 
18
18
  module ChefDK
19
- VERSION = "0.4.0"
19
+ VERSION = "0.5.0.rc.1"
20
20
  end
@@ -19,7 +19,7 @@
19
19
  require "kitchen/provisioner/chef_base"
20
20
 
21
21
  # TODO: chef-dk and kitchen can only co-exist if kitchen and chef-dk agree on
22
- # the version of mixlib-shellout to use. Kitchen currently locked at 1.x,
22
+ # the version of mixlib-shellout to use. Kitchen currently locked at 1.4,
23
23
  # chef-dk is on 2.x
24
24
  require 'chef-dk/policyfile_services/export_repo'
25
25
 
@@ -38,15 +38,19 @@ module Kitchen
38
38
  # * `deployment_group`: `POLICY_NAME-local`
39
39
  # Since it makes no sense to modify these, they are hardcoded elsewhere.
40
40
  default_config :client_rb, {}
41
- default_config :ruby_bindir, "/opt/chef/embedded/bin"
42
-
43
- # Policyfile mode does not support the `-j dna.json` option to
44
- # `chef-client`.
45
- default_config :json_attributes, false
41
+ default_config :json_attributes, true
42
+ default_config :chef_zero_host, nil
46
43
  default_config :chef_zero_port, 8889
47
44
 
48
45
  default_config :chef_client_path do |provisioner|
49
- File.join(provisioner[:chef_omnibus_root], %w[bin chef-client])
46
+ provisioner.
47
+ remote_path_join(%W[#{provisioner[:chef_omnibus_root]} bin chef-client]).
48
+ tap { |path| path.concat(".bat") if provisioner.windows_os? }
49
+ end
50
+
51
+ default_config :ruby_bindir do |provisioner|
52
+ provisioner.
53
+ remote_path_join(%W[#{provisioner[:chef_omnibus_root]} embedded bin])
50
54
  end
51
55
 
52
56
  # Emit a warning that Policyfile stuff is still experimental.
@@ -62,6 +66,7 @@ module Kitchen
62
66
  # (see Base#create_sandbox)
63
67
  def create_sandbox
64
68
  super
69
+ prepare_cookbooks
65
70
  prepare_validation_pem
66
71
  prepare_client_rb
67
72
  end
@@ -85,7 +90,10 @@ module Kitchen
85
90
  args << "--logfile #{config[:log_file]}"
86
91
  end
87
92
 
88
- Util.wrap_command([cmd, *args].join(" "))
93
+ wrap_shell_code(
94
+ [cmd, *args].join(" ").
95
+ tap { |str| str.insert(0, reload_ps1_path) if windows_os? }
96
+ )
89
97
  end
90
98
 
91
99
  private
@@ -112,7 +120,9 @@ module Kitchen
112
120
  #
113
121
  # @api private
114
122
  def policy_exporter
115
- @policy_exporter ||= ChefDK::PolicyfileServices::ExportRepo.new(export_dir: sandbox_path)
123
+ # Must force this because TK by default copies the current cookbook to the sandbox
124
+ # See ChefDK::PolicyfileServices::ExportRepo#assert_export_dir_clean!
125
+ @policy_exporter ||= ChefDK::PolicyfileServices::ExportRepo.new(export_dir: sandbox_path, force: true)
116
126
  end
117
127
 
118
128
  # Writes a fake (but valid) validation.pem into the sandbox directory.
@@ -135,6 +145,8 @@ module Kitchen
135
145
  data["use_policyfile"] = true
136
146
  data["versioned_cookbooks"] = true
137
147
  data["deployment_group"] = "#{policy_exporter.policy_name}-local"
148
+ # TODO this will need to be updated when chef-zero supports erchef paths (policy_group vs policies)
149
+ data["policy_document_native_api"] = false
138
150
 
139
151
  info("Preparing client.rb")
140
152
  debug("Creating client.rb from #{data.inspect}")
@@ -19,10 +19,18 @@ require 'rubygems'
19
19
  require 'rspec/mocks'
20
20
  require 'test_helpers'
21
21
 
22
+ # needed since we stub it for every test
23
+ require 'chef/workstation_config_loader'
24
+
22
25
  RSpec.configure do |c|
23
26
  c.include ChefDK
24
27
  c.include TestHelpers
25
28
 
29
+ # Avoid loading config.rb/knife.rb unintentionally
30
+ c.before(:each) do
31
+ allow_any_instance_of(Chef::WorkstationConfigLoader).to receive(:load)
32
+ end
33
+
26
34
  c.after(:all) { clear_tempdir }
27
35
 
28
36
  c.filter_run :focus => true
@@ -67,6 +67,14 @@ E
67
67
  cli.run
68
68
  end
69
69
 
70
+ def mock_shell_out(exitstatus, stdout, stderr)
71
+ shell_out = double("mixlib_shell_out")
72
+ allow(shell_out).to receive(:exitstatus).and_return(exitstatus)
73
+ allow(shell_out).to receive(:stdout).and_return(stdout)
74
+ allow(shell_out).to receive(:stderr).and_return(stderr)
75
+ shell_out
76
+ end
77
+
70
78
  subject(:cli) do
71
79
  ChefDK::CLI.new(argv).tap do |c|
72
80
  allow(c).to receive(:commands_map).and_return(commands_map)
@@ -106,12 +114,50 @@ E
106
114
  context "given -v" do
107
115
  let(:argv) { %w[-v] }
108
116
 
109
- it "prints the version" do
117
+ let(:tools) {
118
+ {
119
+ "chef-client" => {
120
+ "version_output" => "Chef: 12.0.3",
121
+ "expected_version" => "12.0.3"
122
+ },
123
+ "berks" => {
124
+ "version_output" => "3.2.3",
125
+ "expected_version" => "3.2.3"
126
+ },
127
+ "kitchen" => {
128
+ "version_output" => "Test Kitchen version 1.3.1",
129
+ "expected_version" => "1.3.1"
130
+ }
131
+ }
132
+ }
133
+
134
+ it "does not print versions of tools with missing or errored tools" do
135
+ full_version_message = version_message
136
+ tools.each do |name, version|
137
+ if name == "berks"
138
+ expect(cli).to receive(:shell_out).with("#{name} --version").and_return(mock_shell_out(1, "#{version["version_output"]}", ''))
139
+ full_version_message += "#{name} version: ERROR\n"
140
+ else
141
+ expect(cli).to receive(:shell_out).with("#{name} --version").and_return(mock_shell_out(0, "#{version["version_output"]}", ''))
142
+ full_version_message += "#{name} version: #{version["expected_version"]}\n"
143
+ end
144
+ end
145
+ run_cli(0)
146
+ expect(stdout).to eq(full_version_message)
147
+ end
148
+
149
+ it "prints the version and versions of chef-dk tools" do
150
+ full_version_message = version_message
151
+ tools.each do |name, version|
152
+ expect(cli).to receive(:shell_out).with("#{name} --version").and_return(mock_shell_out(0, "#{version["version_output"]}", ''))
153
+ full_version_message += "#{name} version: #{version["expected_version"]}\n"
154
+ end
110
155
  run_cli(0)
111
- expect(stdout).to eq(version_message)
156
+ expect(stdout).to eq(full_version_message)
112
157
  end
113
158
  end
114
159
 
160
+
115
161
  context "given an invalid option" do
116
162
 
117
163
  let(:argv) { %w[-nope] }
@@ -179,7 +225,7 @@ E
179
225
 
180
226
  let(:ruby_path) { '/opt/chefdk/embedded/bin/ruby' }
181
227
  let(:chefdk_embedded_path) { '/opt/chefdk/embedded/apps/chef-dk' }
182
-
228
+
183
229
  before do
184
230
  stub_const("File::PATH_SEPARATOR", ':')
185
231
  allow(Chef::Util::PathHelper).to receive(:cleanpath) do |path|
@@ -35,7 +35,7 @@ describe ChefDK::Command::GeneratorCommands::App do
35
35
  test/integration/default
36
36
  test/integration/default/serverspec
37
37
  test/integration/default/serverspec/default_spec.rb
38
- test/integration/default/serverspec/spec_helper.rb
38
+ test/integration/helpers/serverspec/spec_helper.rb
39
39
  README.md
40
40
  cookbooks/new_app/Berksfile
41
41
  cookbooks/new_app/chefignore
@@ -35,7 +35,7 @@ describe ChefDK::Command::GeneratorCommands::Cookbook do
35
35
  test/integration/default
36
36
  test/integration/default/serverspec
37
37
  test/integration/default/serverspec/default_spec.rb
38
- test/integration/default/serverspec/spec_helper.rb
38
+ test/integration/helpers/serverspec/spec_helper.rb
39
39
  Berksfile
40
40
  chefignore
41
41
  metadata.rb
@@ -153,14 +153,6 @@ describe ChefDK::Command::GeneratorCommands::Repo do
153
153
  end
154
154
  end
155
155
 
156
- describe "Rakefile" do
157
- let(:file) { "Rakefile" }
158
-
159
- it "loads the common tasks" do
160
- expect(file_contents).to match(/load 'chef\/tasks\/chef_repo\.rake'/)
161
- end
162
- end
163
-
164
156
  describe "chefignore" do
165
157
  let(:file) { "chefignore" }
166
158
 
@@ -189,16 +181,6 @@ describe ChefDK::Command::GeneratorCommands::Repo do
189
181
  end
190
182
  end
191
183
 
192
- describe "certificates" do
193
- describe "README.md" do
194
- let(:file) { "certificates/README.md" }
195
-
196
- it "has the right contents" do
197
- expect(file_contents).to match(/rake ssl_cert FQDN=monitoring.example.com/)
198
- end
199
- end
200
- end
201
-
202
184
  describe "cookbooks" do
203
185
  describe "README.md" do
204
186
  let(:file) { "cookbooks/README.md" }
@@ -215,6 +197,30 @@ describe ChefDK::Command::GeneratorCommands::Repo do
215
197
  end
216
198
  end
217
199
  end
200
+
201
+ describe "example/metadata.rb" do
202
+ let(:file) { "cookbooks/example/metadata.rb" }
203
+
204
+ it "has the right contents" do
205
+ expect(file_contents).to match(/name 'example'/)
206
+ end
207
+ end
208
+
209
+ describe "example/attributes/default.rb" do
210
+ let(:file) { "cookbooks/example/attributes/default.rb" }
211
+
212
+ it "has the right contents" do
213
+ expect(file_contents).to match(/default\["example"\]\["name"\] = "Sam Doe"/)
214
+ end
215
+ end
216
+
217
+ describe "example/recipes/default.rb" do
218
+ let(:file) { "cookbooks/example/recipes/default.rb" }
219
+
220
+ it "has the right contents" do
221
+ expect(file_contents).to match(/log "Welcome to Chef, \#\{node\["example"\]\["name"\]\}!" do/)
222
+ end
223
+ end
218
224
  end
219
225
 
220
226
  describe "data_bags" do
@@ -225,58 +231,48 @@ describe ChefDK::Command::GeneratorCommands::Repo do
225
231
  expect(file_contents).to match(/This directory contains directories of the various data bags/)
226
232
  end
227
233
  end
228
- end
229
234
 
230
- describe "environments" do
231
- describe "README.md" do
232
- let(:file) { "environments/README.md" }
235
+ describe "example_item.json" do
236
+ let(:file) { "data_bags/example/example_item.json" }
233
237
 
234
238
  it "has the right contents" do
235
- expect(file_contents).to match(/This directory is for Ruby DSL and JSON files for environments\./)
239
+ expect(file_contents).to match(/"id": "example_item"/)
236
240
  end
237
241
  end
238
242
  end
239
243
 
240
- describe "roles" do
244
+ describe "environments" do
241
245
  describe "README.md" do
242
- let(:file) { "roles/README.md" }
246
+ let(:file) { "environments/README.md" }
243
247
 
244
248
  it "has the right contents" do
245
- expect(file_contents).to match(/Create roles here/)
249
+ expect(file_contents).to match(/Create environments here, in either the Role Ruby DSL \(\.rb\) or JSON \(\.json\) files\./)
246
250
  end
247
251
  end
248
- end
249
252
 
250
- describe "config" do
251
- describe "rake.rb" do
252
- let(:file) { "config/rake.rb" }
253
+ describe "example.json" do
254
+ let(:file) { "environments/example.json" }
253
255
 
254
- it "has the preamble" do
255
- expect(file_contents).to match(/Configure the Rakefile's tasks\./)
256
- end
257
-
258
- it "defaults the copyright to The Authors" do
259
- expect(file_contents).to match(/COMPANY_NAME = "The Authors"/)
260
- end
261
-
262
- it "defaults the license to all_rights" do
263
- expect(file_contents).to match(/NEW_COOKBOOK_LICENSE = :all_rights/)
256
+ it "has the right contents" do
257
+ expect(file_contents).to match(/"description": "This is an example environment defined as JSON"/)
264
258
  end
259
+ end
260
+ end
265
261
 
266
- context "with a license" do
267
- let(:argv) { ["new_repo", "-I", "gplv3" ] }
262
+ describe "roles" do
263
+ describe "README.md" do
264
+ let(:file) { "roles/README.md" }
268
265
 
269
- it "uses the license" do
270
- expect(file_contents).to match(/NEW_COOKBOOK_LICENSE = :gplv3/)
271
- end
266
+ it "has the right contents" do
267
+ expect(file_contents).to match(/Create roles here, in either the Role Ruby DSL \(\.rb\) or JSON \(\.json\) files\./)
272
268
  end
269
+ end
273
270
 
274
- context "with a copyright holder" do
275
- let(:argv) { ["new_repo", "-C", "GiantCo, Inc." ] }
271
+ describe "example.json" do
272
+ let(:file) { "roles/example.json" }
276
273
 
277
- it "uses the copyright holder" do
278
- expect(file_contents).to match(/COMPANY_NAME = "GiantCo, Inc\."/)
279
- end
274
+ it "has the right contents" do
275
+ expect(file_contents).to match(/"description": "This is an example role defined as JSON"/)
280
276
  end
281
277
  end
282
278
  end