Puppet Faces is one of the features I am proudest to deliver in the new Puppet 2.7.0 release. (Well, the RC series heading toward release, anyway.) They represent a new API for creating subcommands, as well as extending existing subcommands to add extra capabilities‚ without having to modify the supplied source code.
In this post, and the rest of the series, I am going to dive right on down deep into the technical details of Faces and how they can be used to program (and reprogram) a whole bunch of Puppet.
The key capabilities of Faces that make them important are:
- Faces provide a defined, documented, robust API to the internals of Puppet.
- They let you have that same robust, documented power for your extensions.
- Faces have solid backward compatibility baked in to their core.
- Faces give you access to a whole mess of capabilities for free.
- You get a command line interface to your Face for free...
- ...and we have a whole lot more coming down the pipe, too.
Faces are all over backward compatibility!
While we were building the Faces system we put a whole lot of thought into how we can make it easy for you to extend Puppet ‚ and how to make sure that we don't break your extensions when we need to change part of that API.
One of the first things you will run across is version numbers. When you refer to a Face, either to use it, or to extend it, you need to specify what version you are talking about:
facts = Puppet::Face[:facts, '0.0.1'] status = Puppet::Face[:status, '0.0.1']
On the plus side, a Face is just a regular Ruby object with a whole bunch of behaviour and convention poured on top, so you can assign them to a local variable and then call it any number of times. So, don't worry about having to type out that version number more than once in your code, eh.
Those version numbers show up when you are looking at writing or extending Faces, too; building up the face, or adding an action, requires you to specify what version of the Face it is built on:
Puppet::Face.define(:node, '0.0.1') do action :example do option "--[no-]override" option "--fruit FLAVOUR" when_invoked |a, b, c, options| # Your code goes here... end end end
Now, the obvious question is why? Why do we force you to declare the version of the Face you are extending here? Our answer is pretty simple: because it means that we can change the current version of the face and things that you write will just keep working, unchanged.
I think that is one of the finest features we are delivering, a guarantee of backward compatibility in your extensions. Internally, we implement this by running your action in the context of the Face it was defined on: if you expect the
0.0.1 API we deliver, and things just work.
Obviously, if you want to use cool new features in a newer version of the API you have to migrate your code to handle any other changes ... but at least the basics still work as you expected. Unless you are really clever...
Puppet::Face.define(:example, '1.0.0') do action :wicked_smart do # documentation omitted for brevity... when_invoked do |alpha, beta, options| # This totally works, but don't tell anyone I told you. advanced_example = Puppet::Face[:example, '2.0.0'] advanced_example.new_feature current_action_name(alpha, beta) # You can even do this, if you want ^W need to: ;) current_action_name(alpha, beta) advanced_example.current_action_name(beta, alpha) end end end
Anyway, that aside put aside for now, versioning is a huge part of why I think that Faces are cool: it is universal, it is powerful, and it lets us keep things working.
Our promise about versions
Any promise goes two ways, and versioning Faces like this is no exception. We don't treat it lightly: we promise that in return for the extra little bit of work it takes to use the versioned API that we will try our level best to keep old API versions around and working for as long as possible.
(They might, heck, will‚ end up rewritten to just call up to the newer version of the API transparently, but you don't need to care. We will keep the external API compatible so your code just works‚ the whole time.)
Which is pretty cool and all, but that isn't it: we also promise to use our powers for evil, not just for good. Now that things you write are good and stable with versioning, we can release newer versions of the API sooner.
That means that you should see less time between a great idea and getting to use it. (Plus, you can reliably work out if it isn't there on a machine, and fall back in your own code.)
Faces Versioning FAQ
Do I really have to specify a version number?
Technically, yes, but. We have two ways to do something smarter than giving the full version number. One is the ability to specify only part of the version number, and have the rest match "any version":
- "1.0.1" will only match exactly version 1.0.1.
- "1.0" will match version 1.0.1, and also 1.0.2, but not 1.1.0.
- "1" will match anything with the major version 1.
We also offer, but do not encourage, the ability to refer to the
:current version of a particular face, which will return the latest "officially blessed" version of the face found in your Ruby load path.
We don't really recommend using it, though: saying "use
:current" is the same as saying "please break when the API changes". I don't know about you, but that usually isn't what I want to say when I write code ‚ even throw-away scripts. (I guess mostly because I noticed that I almost never get to throw those ones away, while the ones I expect to last? Pffft, gone.)
So, I would encourage you to always specify at least the major number of your version. It means less surprises, and less headaches, for you in the long run.
What do your version numbers mean?
The great thing about standards is that there are so many to choose from, as the saying goes, and version numbers are no different. Lots of people have their own ideas about what they mean, how they work, and how you should sort them.
Here at Puppet Labs we love inventing a new standard as much as the next programmer, but this time sense prevailed: we picked up the Semantic Versioning Specification and used that to define our Face versioning. (Plus, it was invented by one of the folks behind GitHub, and they are wicked-smart and all.)
Specifically, each digit has a defined meaning:
- digit one, major: incremented for a backwards-incompatible API change.
- digit two, minor: incremented for a backwards-compatible API change.
- digit three, patch: incremented when change does not touch the API.
Not that you need to worry too much: if you want something simple, just use
2.0.0, and so forth in your code, and pretend the rest of the digits don't actually exist.