1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
|
<!---
This file is reset every time a new release is done. This file describes changes that have not yet been released.
Example Doc Change:
### Headline for the required change
Description of the required change.
-->
### chef-client -j JSON
Add to the [description of chef-client options](https://docs.chef.io/ctl_chef_client.html#options):
> This option can also be used to set a node's `chef_environment`. For example,
running `chef-client -j /path/to/file.json` where `/path/to/file.json` is
similar to:
```
{
"chef_environment": "pre-production"
}
```
will set the node's environment to `"pre-production"`.
> *Note that the environment specified by `chef_environment` in your JSON will
take precedence over an environment specified by `-E ENVIROMENT` when both options
are provided.*
### Resources Made Easy
Resources are central to Chef. The system is extensible so that you can write
your own reusable resources, and use them in your recipes, and even publish them
so that others can use them too!
However, writing these resources has not been as easy as we would have liked. In
Chef 12.5, we are fixing this with a large number of DSL improvements designed
to reduce the number of things you need to type and think about when you create
a resource. Resources should be your go-to solution for many Chef problems, and
these changes make them easy enough to dash off in an instant, while retaining
all the power you're accustomed to.
The process to create a resource is now:
1. Make a resource file in your cookbook (like `resources/my_resource.rb`).
2. Add the recipes defining your actions using the `action :create <recipe>` DSL.
3. Add properties so the user can tweak some knobs on your resource (like paths,
or preferences), using the `property :my_property, <type>, <options>` DSL.
4. Use the resource in your recipe!
There are other things you can do, but this is the most basic (and the first)
thing you will start with.
Let's demonstrate the new features by taking a simple recipe from the awesome
[learnchef tutorial](https://learn.chef.io/learn-the-basics/rhel/configure-a-package-and-service/),
and turning it into a reusable resource:
```ruby
package 'httpd'
service 'httpd' do
action [:enable, :start]
end
file '/var/www/html/index.html' do
content '<html>
<body>
<h1>hello world</h1>
</body>
</html>'
end
service 'iptables' do
action :stop
end
```
We'll design a resource that lets you write this recipe instead:
```ruby
single_page_website 'mysite' do
homepage '<html>
<body>
<h1>hello world</h1>
</body>
</html>'
end
```
#### Declaring the Resource
The first thing we do is declare the resource. We can do that by creating an
empty file, `resources/single_page_website.rb`, in our cookbook.
When you do this, the `single_page_website` resource will work in all recipes!
```ruby
single_page_website 'mysite'
```
It won't do anything yet, though :)
#### Declaring an Action
Let's make our resource do something. To start with, we'll just have it do exactly
what the learnchef tutorial does, but in the resource. Put this in
`resources/single_page_website.rb`:
```ruby
action :create do
package 'httpd'
service 'httpd' do
action [:enable, :start]
end
file '/var/www/html/index.html' do
content '<html>
<body>
<h1>hello world</h1>
</body>
</html>'
end
service 'iptables' do
action :stop
end
end
```
Now, your simple recipe can use this resource to do what learnchef did:
```ruby
single_page_website 'mysite'
```
We've got ourselves an httpd!
You will notice the only thing we've done is to add `action :create` around the
recipe. The `action` keyword lets you declare a recipe inline, which will be
executed when the user uses your resource in a recipe.
#### Declaring a resource property: "homepage"
This isn't super reusable yet--you might want your webpage to say something other
than "hello world". Let's add a couple of properties for that, by putting this
at the top of `resources/single_page_website`, and modifying the recipe to use
"title" and "body":
```ruby
property :homepage, String, default: '<h1>hello world</h1>'
action :create do
package 'httpd'
service 'httpd' do
action [:enable, :start]
end
file '/var/www/html/index.html' do
content homepage
end
service 'iptables' do
action :stop
end
end
```
Now you can run this recipe:
```ruby
single_page_website 'mysite' do
homepage '<h1>My own page</h1>'
end
```
And you've got a website with your stuff!
What you've done here is add *properties*. Properties are the *desired state* of
a resource, in this case, `homepage` defines the text on the website. When you
add a property, you're letting a user give it whatever value they want.
When you define a property, there are three bits:
`property :<name>, <type>, <options>`. *Name* defines the name of the property,
so that people can set the property using `name <value>` when they use your
resource. *Type* defines the type of the property: for example, String, Integer
and Array are all possible types. Type is optional. *Options* define a large
number of validation and other options. You've seen `default` already now,
but there are a ton of others.
#### Adding another property: "not_found_page"
What if we want a custom 404 page for when people try to go to other pages in
our website? Let's add one more property, to make this even nicer:
```ruby
property :homepage, String, default: '<h1>hello world</h1>'
property :not_found_page, String, default: '<h1>No such page! Sorry. 404.</h1>'
action :create do
package 'httpd'
service 'httpd' do
action [:enable, :start]
end
file '/var/www/html/index.html' do
content homepage
end
# These together tell Apache to use your custom 404 page:
file '/var/www/html/404.html' do
content not_found_page
end
file '/var/www/html/.htaccess' do
content 'ErrorDocument 404 /404.html'
end
service 'iptables' do
action :stop
end
end
```
Now you can run this recipe:
```ruby
single_page_website 'mysite' do
homepage '<h1>My own page</h1>'
not_found_page '<h1>Grr. Page not found. Sorry. (404)</h1>'
end
```
#### Adding another action: "stop"
What if we want to stop the website? Just add another action into the bottom of
`resources/single_page_website.rb`:
```ruby
action :stop do
service 'httpd' do
action :stop
end
end
```
This action looks a lot like the other.
There are a ton of other things you can do to create resources, but this should
give you a pretty basic idea.
### Advanced Resource Capabilities
#### Ruby Developers: Resources as Classes
If you are a Ruby developer, we've made it easier to create a Resource outside
of a cookbook (or in a library) by declaring a class! Declare
`class SinglePageWebsite < Chef::Resource` and put the entire resource
declaration inside, and the `single_page_website` resource will work!
#### Reading the current value: load_current_value
There is a pitfall inherent in a resource, where users will sometimes omit a
property from a resource, and become surprised when the system overwrites it
with the default! For example, if your website already exists, this recipe
will replace the *homepage* with "hello world":
```ruby
single_page_website 'mysite' do
not_found_page '<h1>nice</h1>'
end
```
It's not at all clear that that's what the user wanted--they didn't say anything
about the homepage, so why did something happen to it?
To guard against this, you can implement `load_current_value` in your resource.
Put this in `resources/single_page_website.rb`:
```ruby
load_current_value do
if File.exist?('/var/www/html/index.html')
homepage IO.read('/var/www/html/index.html')
end
if File.exist?('/var/www/html/404.html')
not_found_page IO.read('/var/www/html/404.html')
end
end
```
Now, the above recipe knows what the current homepage is, and will not change it!
This capability is also used for several other things, including reporting (to
describe what changed) and pure Ruby actions.
#### Pure Ruby Actions
Some resources need to talk directly to Ruby to do their dirty work, rather than using other resources. In those cases, you need to:
- Make the updates only if the user specified properties that *need* to change.
- Make sure and call updates if the resource does not exist (need to be created).
- Print useful green text if the update is happening.
- Not actually make any changes in why-run mode!
`converge_if_changed` handles all of the above by comparing the user's desired
property values against the *current* value as loaded by `load_current_value`.
Simply wrap the part of your recipe that does a set in `converge_if_changed`.
As an example, here is a basic `my_file` resource that creates a file with the
given content:
```ruby
# resources/my_file.rb
property :path, String, name_property: true
property :content, String
load_current_value do
if File.exist?(path)
content IO.read(path)
end
end
action :create do
converge_if_changed do
IO.write(path, content)
end
end
```
The above code will only call `IO.write` if the file does not exist, or if the
user specified content that is different from what is on disk. It will print out
something like this, showing the changes:
```ruby
Recipe: basic_chef_client::block
* my_file[blah] action create
- update my_file[blah]
- set content to "hola mundo" (was "hello world")
```
##### Handling Multiple Operations
If you have two separate, expensive operations to handle converge, `converge_if_changed`
can be called multiple times with multiple properties. Adding `mode` to `my_file`
demonstrates this:
```ruby
# resources/my_file.rb
property :path, String, name_property: true
property :content, String
property :mode, String
load_current_value do
if File.exist?(path)
content IO.read(path)
mode File.stat(path).mode
end
end
action :create do
# Only change content here
converge_if_changed :content do
IO.write(path, content)
end
# Only change mode here
converge_if_changed :mode do
File.chmod(mode, path)
end
end
```
|