Using the wash gem to write your external plugins
Note: This blog post’s been updated with the meta
attribute => partial metadata change in Wash 0.18.0 and wash-ruby 0.3.0.
Wash ships with plugins that let you interact with your AWS, GCP, Docker, and Kubernetes resources. You can also write your own plugins via the external plugin interface. An external plugin consists of a plugin script. Wash shells out to this script whenever it needs to invoke an entry’s supported action (like list), or if it needs to query something about the entry (like its metadata). In a more general sense, Wash shells out to the plugin script whenever it needs to invoke an entry’s supported method. All plugin script invocations adopt the following calling convention
<plugin_script> <method> <path> <state> <args...>
where
-
<method>
is the Wash method that’s being invoked. This includes Wash actions likelist
andexec
, and also non-Wash actions likeschema
andmetadata
. -
<path>
is the entry’s filesystem path rooted at Wash’s mountpoint. For example,/my_plugin
would be themy_plugin
’s plugin root./my_plugin/foo
would be thefoo
child of themy_plugin
entry. -
<state>
consists of the minimum amount of information required to reconstruct the entry inside the plugin. It can be any string. For example,'{"klass": "Root"}'
could be a JSON object representing the plugin root in a higher-level programming language like Python or Ruby. -
<args...>
are<method>
’s arguments. For example if<method>
isexec
, then the exec’ed command would be included in<args...>
.
Now let <entry> = <path> <state>
. Then the plugin script’s usage becomes
<plugin_script> <method> <entry> <args...>
From this usage, we see that <path>
and <state>
are two different representations of an entry. <path>
is useful for simple plugins where reconstructing the entry is easy. <state>
is useful for more complicated plugins where entries could be represented as classes.
If you’re a Ruby developer, then the wash gem makes external plugin development easy by letting you model your entries as classes. We’ll show you what this looks like by walking you through some of the puppetwash plugin’s implementation, specifically the puppetwash.rb file.
To facilitate the following discussion, here’s what the puppetwash
plugin looks like
wash . ❯ stree puppet
puppet
└── [pe_instance]
└── nodes_dir
└── [node]
├── catalog
├── facts_dir
│ └── [fact]
└── reports_dir
└── [report]
If you’ve used Puppet before, then much of the above output should look familiar (and you should definitely try out the plugin!) If not, then don’t worry – this blog post is written for a general audience so you should still be able to extract some useful information from it.
Each label that you see in stree
’s output corresponds to a class. For example, the puppet
label (plugin root) corresponds to the Puppetwash
class. The pe_instance
label (a PE instance) corresponds to the PEInstance
class. Similarly, the report
label (a node’s report) corresponds to the Report
class.
Let’s take a look at each class’s implementations. Note that we’re omitting some code for brevity.
Notice that
- Each class extends the
Wash::Entry
class. - Entry schemas are declared via class-level helpers like
label
,is_singleton
, andparent_of
. - Each Wash method corresponds to an instance method in the class. For example, a report’s
read
action is implemented byReport#read
. Similarly, a PE instance’slist
action is implemented byPEInstance#list
. - Entry attributes are declared via the
attributes
helper and can be set/accessed as instance-level fields. For example, a report’smtime
attributes is set in the constructor (initialize
method). - The
state
helper lets you specify instance-level fields that should be serialized as part of thestate
key in the entry’s JSON (this is also what’s passed into the<state>
argument in plugin script invocations). Thewash gem
will set these fields after it reconstructs your entry. Thus, you can safely access those fields’ values when implementing the entry’s methods. For example, the@pe_name
field inReport
is initialized in the constructor and then referenced inReport#read
.
The Wash::Entry
class makes all of this possible. It also comes with even more helpers that let you do other things like specify cache TTLs (see Wash::Entry#cache_ttls
) or any prefetched methods (see Wash::Entry#prefetch
).
Recall that plugin script invocations adopt the following calling convention
<plugin_script> <method> <entry> <args...>
where <entry> = <path> <state>
.
All plugin script invocations are handled by the Wash.run
function. Here’s how Wash.run
is used in puppetwash.rb
Wash.run(Puppetwash, ARGV)
And here’s Wash.run
’s implementation. At a high-level, Wash.run
- Sets the
SIGINT
andSIGTERM
handlers to ensure proper cleanup onCtrl+C
or on aSIGTERM
signal sent by Wash - Invokes the specified method
- If the method is
init
, thenWash.run
- Creates the plugin root object using the provided class (
Puppetwash
) - Invokes its
init
method (Puppetwash#init
). Note that this is different from Ruby’sinitialize
method, which defines the class constructor. - Prints the root object’s JSON to stdout
- Creates the plugin root object using the provided class (
- Otherwise,
Wash.run
- Reconstructs the entry object from the provided
state
argument - Delegates to
Method.invoke
, which effectivelly calls<entry>.<method>(<args>)
- Reconstructs the entry object from the provided
- If the method is
Thus, we see that the wash gem
frees you from having to do all of this plumbing yourself. All you have to do is define a few Ruby classes, implement some methods, then pass everything over to Wash.run
to handle the rest. Try it out and let us know what you think!