I had some legacy unnecessary boolean for sending everywhere. Not sure why I never re-read it, it's so easy to just copy and paste and carry on.
827 lines
32 KiB
Markdown
827 lines
32 KiB
Markdown
# Resource guide
|
|
|
|
## Overview
|
|
|
|
The `mgmt` tool has built-in resource primitives which make up the building
|
|
blocks of any configuration. Each instance of a resource is mapped to a single
|
|
vertex in the resource [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
|
|
This guide is meant to instruct developers on how to write a brand new resource.
|
|
Since `mgmt` and the core resources are written in golang, some prior golang
|
|
knowledge is assumed.
|
|
|
|
## Theory
|
|
|
|
Resources in `mgmt` are similar to resources in other systems in that they are
|
|
[idempotent](https://en.wikipedia.org/wiki/Idempotence). Our resources are
|
|
uniquely different in that they can detect when their state has changed, and as
|
|
a result can run to revert or repair this change instantly. For some background
|
|
on this design, please read the
|
|
[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
|
on the subject.
|
|
|
|
## Resource Prerequisites
|
|
|
|
### Imports
|
|
|
|
You'll need to import a few packages to make writing your resource easier. Here
|
|
is the list:
|
|
|
|
```
|
|
"github.com/purpleidea/mgmt/engine"
|
|
"github.com/purpleidea/mgmt/engine/traits"
|
|
```
|
|
|
|
The `engine` package contains most of the interfaces and helper functions that
|
|
you'll need to use. The `traits` package contains some base functionality which
|
|
you can use to easily add functionality to your resource without needing to
|
|
implement it from scratch.
|
|
|
|
### Resource struct
|
|
|
|
Each resource will implement methods as pointer receivers on a resource struct.
|
|
The naming convention for resources is that they end with a `Res` suffix.
|
|
|
|
The resource struct should include an anonymous reference to the `Base` trait.
|
|
Other `traits` can be added to the resource to add additional functionality.
|
|
They are discussed below.
|
|
|
|
You'll most likely want to store a reference to the `*Init` struct type as
|
|
defined by the engine. This is data that the engine will provide to your
|
|
resource on Init.
|
|
|
|
Lastly you should define the public fields that make up your resource API, as
|
|
well as any private fields that you might want to use throughout your resource.
|
|
Do _not_ depend on global variables, since multiple copies of your resource
|
|
could get instantiated.
|
|
|
|
You'll want to add struct tags based on the different frontends that you want
|
|
your resources to be able to use. Some frontends can infer this information if
|
|
it is not specified, but others cannot, and some might poorly infer if the
|
|
struct name is ambiguous.
|
|
|
|
If you'd like your resource to be accessible by the `YAML` graph API (GAPI),
|
|
then you'll need to include the appropriate YAML fields as shown below. This is
|
|
used by the `puppet` compiler as well, so make sure you include these struct
|
|
tags if you want existing `puppet` code to be able to run using the `mgmt`
|
|
engine.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
type FooRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
traits.Groupable
|
|
traits.Refreshable
|
|
|
|
init *engine.Init
|
|
|
|
Whatever string `lang:"whatever" yaml:"whatever"` // you pick!
|
|
Baz bool `lang:"baz" yaml:"baz"` // something else
|
|
|
|
something string // some private field
|
|
}
|
|
```
|
|
|
|
## Resource API
|
|
|
|
To implement a resource in `mgmt` it must satisfy the
|
|
[`Res`](https://github.com/purpleidea/mgmt/blob/master/engine/resources.go)
|
|
interface. What follows are each of the method signatures and a description of
|
|
each.
|
|
|
|
### Default
|
|
|
|
```golang
|
|
Default() engine.Res
|
|
```
|
|
|
|
This returns a populated resource struct as a `Res`. It shouldn't populate any
|
|
values which already get a good default as the respective golang zero value. In
|
|
general it is preferable if the zero values make for the correct defaults.
|
|
(This is to say, resources are designed to behave safely and intuitively
|
|
when parameters take a zero value, whenever this is possible.)
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *FooRes) Default() engine.Res {
|
|
return &FooRes{
|
|
Answer: 42, // sometimes, defaults shouldn't be the zero value
|
|
}
|
|
}
|
|
```
|
|
|
|
### Validate
|
|
|
|
```golang
|
|
Validate() error
|
|
```
|
|
|
|
This method is used to validate if the populated resource struct is a valid
|
|
representation of the resource kind. If it does not conform to the resource
|
|
specifications, it should return an error. If you notice that this method is
|
|
quite large, it might be an indication that you should reconsider the parameter
|
|
list and interface to this resource. This method is called by the engine
|
|
_before_ `Init`. It can also be called occasionally after a Send/Recv operation
|
|
to verify that the newly populated parameters are valid. Remember not to expect
|
|
access to the outside world when using this.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Validate reports any problems with the struct definition.
|
|
func (obj *FooRes) Validate() error {
|
|
if obj.Answer != 42 { // validate whatever you want
|
|
return fmt.Errorf("expected an answer of 42")
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Init
|
|
|
|
```golang
|
|
Init() error
|
|
```
|
|
|
|
This is called to initialize the resource. If something goes wrong, it should
|
|
return an error. It should do any resource specific work such as initializing
|
|
channels, sync primitives, or anything else that is relevant to your resource.
|
|
If it is not need throughout, it might be preferable to do some initialization
|
|
and tear down locally in either the Watch method or CheckApply method. The
|
|
choice depends on your particular resource and making the best decision requires
|
|
some experience with mgmt. If you are unsure, feel free to ask an existing
|
|
`mgmt` contributor. During `Init`, the engine will pass your resource a struct
|
|
containing some useful data and pointers. You should save a copy of this pointer
|
|
since you will need to use it in other parts of your resource.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Init initializes the Foo resource.
|
|
func (obj *FooRes) Init(init *engine.Init) error
|
|
obj.init = init // save for later
|
|
|
|
// run the resource specific initialization, and error if anything fails
|
|
if some_error {
|
|
return err // something went wrong!
|
|
}
|
|
return nil
|
|
}
|
|
```
|
|
|
|
This method is always called after `Validate` has run successfully, with the
|
|
exception that we can't prevent a malicious or buggy `libmgmt` user to not run
|
|
this. In other words, you should expect `Validate` to have run first, but you
|
|
shouldn't allow `Init` to dangerously `rm -rf /$the_world` if your code only
|
|
checks `$the_world` in `Validate`. Remember to always program safely!
|
|
|
|
### Cleanup
|
|
|
|
```golang
|
|
Cleanup() error
|
|
```
|
|
|
|
This is called to cleanup after the resource. It is usually not necessary, but
|
|
can be useful if you'd like to properly close a persistent connection that you
|
|
opened in the `Init` method and were using throughout the resource. It is *not*
|
|
the shutdown signal that tells the resource to exit. That happens in the Watch
|
|
loop.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *FooRes) Cleanup() error {
|
|
err := obj.conn.Close() // close some internal connection
|
|
obj.someMap = nil // free up some large data structure from memory
|
|
return err
|
|
}
|
|
```
|
|
|
|
You should probably check the return errors of your internal methods, and pass
|
|
on an error if something went wrong.
|
|
|
|
### CheckApply
|
|
|
|
```golang
|
|
CheckApply(ctx context.Context, apply bool) (checkOK bool, err error)
|
|
```
|
|
|
|
`CheckApply` is where the real _work_ is done. Under normal circumstances, this
|
|
function should check if the state of this resource is correct, and if so, it
|
|
should return: `(true, nil)`. If the `apply` variable is set to `true`, then
|
|
this means that we should then proceed to run the changes required to bring the
|
|
resource into the correct state. If the `apply` variable is set to `false`, then
|
|
the resource is operating in _noop_ mode and _no operational changes_ should be
|
|
made! The ctx should be monitored in case a shutdown has been requested. This
|
|
may be used if a timeout occurred, or if the user shutdown the engine.
|
|
|
|
After having executed the necessary operations to bring the resource back into
|
|
the desired state, or after having detected that the state was incorrect, but
|
|
that changes can't be made because `apply` is `false`, you should then return
|
|
`(false, nil)`.
|
|
|
|
You must cause the resource to converge during a single execution of this
|
|
function. If you cannot, then you must return an error! The exception to this
|
|
rule is that if an external force changes the state of the resource while it is
|
|
being remedied, it is possible to return from this function even though the
|
|
resource isn't now converged. This is not a bug, as the resources `Watch`
|
|
facility will detect the new change, ultimately resulting in a subsequent call
|
|
to `CheckApply`.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// CheckApply does the idempotent work of checking and applying resource state.
|
|
func (obj *FooRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// check the state
|
|
if state_is_okay { return true, nil } // done early! :)
|
|
|
|
// state was bad
|
|
|
|
if !apply { return false, nil } // don't apply, we're in noop mode
|
|
|
|
if any_error { return false, err } // anytime there's an err!
|
|
|
|
// do the apply!
|
|
return false, nil // after success applying
|
|
}
|
|
```
|
|
|
|
The `CheckApply` function is called by the `mgmt` engine when it believes a call
|
|
is necessary. Under certain conditions when a `Watch` call does not invalidate
|
|
the state of the resource, and no refresh call was sent, its execution might be
|
|
skipped. This is an engine optimization, and not a bug. It is mentioned here in
|
|
the documentation in case you are confused as to why a debug message you've
|
|
added to the code isn't always printed.
|
|
|
|
#### Paired execution
|
|
|
|
For many resources it is not uncommon to see `CheckApply` run twice in rapid
|
|
succession. This is usually not a pathological occurrence, but rather a healthy
|
|
pattern which is a consequence of the event system. When the state of the
|
|
resource is incorrect, `CheckApply` will run to remedy the state. In response to
|
|
having just changed the state, it is usually the case that this repair will
|
|
trigger the `Watch` code! In response, a second `CheckApply` is triggered, which
|
|
will likely find the state to now be correct.
|
|
|
|
#### Summary
|
|
|
|
* Anytime an error occurs during `CheckApply`, you should return `(false, err)`.
|
|
* If the state is correct and no changes are needed, return `(true, nil)`.
|
|
* You should only make changes to the system if `apply` is set to `true`.
|
|
* After checking the state and possibly applying the fix, return `(false, nil)`.
|
|
* Returning `(true, err)` is a programming error and can have a negative effect.
|
|
|
|
### Watch
|
|
|
|
```golang
|
|
Watch(ctx context.Context) error
|
|
```
|
|
|
|
`Watch` is a main loop that runs and sends messages when it detects that the
|
|
state of the resource might have changed. To send a message you should write to
|
|
the input event channel using the `Event` helper method. The Watch function
|
|
should run continuously until a shutdown message is received. If at any time
|
|
something goes wrong, you should return an error, and the `mgmt` engine will
|
|
handle possibly restarting the main loop based on the `retry` meta parameter.
|
|
|
|
It is better to send an event notification which turns out to be spurious, than
|
|
to miss a possible event. Resources which can miss events are incorrect and need
|
|
to be re-engineered so that this isn't the case. If you have an idea for a
|
|
resource which would fit this criteria, but you can't find a solution, please
|
|
contact the `mgmt` maintainers so that this problem can be investigated and a
|
|
possible system level engineering fix can be found.
|
|
|
|
You may have trouble deciding how much resource state checking should happen in
|
|
the `Watch` loop versus deferring it all to the `CheckApply` method. You may
|
|
want to put some simple fast path checking in `Watch` to avoid generating
|
|
obviously spurious events, but in general it's best to keep the `Watch` method
|
|
as simple as possible. Contact the `mgmt` maintainers if you're not sure.
|
|
|
|
If the resource is activated in `polling` mode, the `Watch` method will not get
|
|
executed. As a result, the resource must still work even if the main loop is not
|
|
running.
|
|
|
|
You must make sure to cleanup any running code or goroutines before Watch exits.
|
|
|
|
#### Select
|
|
|
|
The lifetime of most resources `Watch` method should be spent in an infinite
|
|
loop that is bounded by a `select` call. The `select` call is the point where
|
|
our method hands back control to the engine (and the kernel) so that we can
|
|
sleep until something of interest wakes us up. In this loop we must wait until
|
|
we get a shutdown event from the engine via the `<-ctx.Done()` channel, which
|
|
closes when we'd like to shut everything down. At this point you should cleanup,
|
|
and let `Watch` close.
|
|
|
|
#### Events
|
|
|
|
If the `<-ctx.Done()` channel closes, we should shutdown our resource. When we
|
|
want to send an event, we use the `Event` helper function. This automatically
|
|
marks the resource state as `dirty`. If you're unsure, it's not harmful to send
|
|
the event. This will ultimately cause `CheckApply` to run. This method can block
|
|
if the resource is being paused.
|
|
|
|
#### Startup
|
|
|
|
Once the `Watch` function has finished starting up successfully, it is important
|
|
to generate one event to notify the `mgmt` engine that we're now listening
|
|
successfully, so that it can run an initial `CheckApply` to ensure we're safely
|
|
tracking a healthy state and that we didn't miss anything when `Watch` was down
|
|
or from before `mgmt` was running. You must do this by calling the
|
|
`obj.init.Running` method.
|
|
|
|
#### Converged
|
|
|
|
The engine might be asked to shutdown when the entire state of the system has
|
|
not seen any changes for some duration of time. The engine can determine this
|
|
automatically, but each resource can block this if it is absolutely necessary.
|
|
If you need this functionality, please contact one of the maintainers and ask
|
|
about adding this feature and improving these docs right here.
|
|
|
|
This particular facility is most likely not required for most resources. It may
|
|
prove to be useful if a resource wants to start off a long operation, but avoid
|
|
sending out erroneous `Event` messages to keep things alive until it finishes.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Watch is the listener and main loop for this resource.
|
|
func (obj *FooRes) Watch(ctx context.Context) error {
|
|
// setup the Foo resource
|
|
var err error
|
|
if err, obj.foo = OpenFoo(); err != nil {
|
|
return err // we couldn't startup
|
|
}
|
|
defer obj.whatever.CloseFoo() // shutdown our Foo
|
|
|
|
// notify engine that we're running
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
for {
|
|
select {
|
|
// the actual events!
|
|
case event := <-obj.foo.Events:
|
|
if !is_an_event {
|
|
continue // skip event
|
|
}
|
|
// send below...
|
|
|
|
// event errors
|
|
case err := <-obj.foo.Errors:
|
|
return err // will cause a retry or permanent failure
|
|
|
|
case <-ctx.Done(): // signal for shutdown request
|
|
return nil
|
|
}
|
|
|
|
obj.init.Event() // notify engine of an event (this can block)
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Summary
|
|
|
|
* Remember to call `Running` when the `Watch` is running successfully.
|
|
* Remember to process internal events and shutdown promptly if asked to.
|
|
* Ensure the design of your resource is well thought out.
|
|
* Have a look at the existing resources for a rough idea of how this all works.
|
|
|
|
### Cmp
|
|
|
|
```golang
|
|
Cmp(engine.Res) error
|
|
```
|
|
|
|
Each resource must have a `Cmp` method. It is an abbreviation for `Compare`. It
|
|
takes as input another resource and must return whether they are identical or
|
|
not. This is used for identifying if an existing resource can be used in place
|
|
of a new one with a similar set of parameters. In particular, when switching
|
|
from one graph to a new (possibly identical) graph, this avoids recomputing the
|
|
state for resources which don't change or that are sufficiently similar that
|
|
they don't need to be swapped out.
|
|
|
|
In general if all the resource properties are identical, then they usually don't
|
|
need to be changed. On occasion, not all of them need to be compared, in
|
|
particular if they store some generated state, or if they aren't significant in
|
|
some way.
|
|
|
|
If the resource is identical, then you should return `nil`. If it is not, then
|
|
you should return a short error message which gives the reason it differs.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// Cmp compares two resources and returns if they are equivalent.
|
|
func (obj *FooRes) Cmp(r engine.Res) error {
|
|
// we can only compare FooRes to others of the same resource kind
|
|
res, ok := r.(*FooRes)
|
|
if !ok {
|
|
return fmt.Errorf("not a %s", obj.Kind())
|
|
}
|
|
|
|
if obj.Whatever != res.Whatever {
|
|
return fmt.Errorf("the Whatever param differs")
|
|
}
|
|
if obj.Flag != res.Flag {
|
|
return fmt.Errorf("the Flag param differs")
|
|
}
|
|
|
|
return nil // they must match!
|
|
}
|
|
```
|
|
|
|
## Traits
|
|
|
|
Resources can have different `traits`, which means they can be extended to have
|
|
additional functionality or special properties. Those special properties are
|
|
usually added by extending your resource so that it is compatible with
|
|
additional interface that contain the `Res` interface. Each of these interfaces
|
|
represents the additional functionality. Since in most cases this requires some
|
|
common boilerplate, you can usually get some or most of the functionality by
|
|
embedding the correct trait struct anonymously in your struct. This is shown in
|
|
the struct example above. You'll always want to include the `Base` trait in all
|
|
resources. This provides some basics which you'll always need.
|
|
|
|
What follows are a list of available traits.
|
|
|
|
### Refreshable
|
|
|
|
Some resources may choose to support receiving refresh notifications. In general
|
|
these should be avoided if possible, but nevertheless, they do make sense in
|
|
certain situations. Resources that support these need to verify if one was sent
|
|
during the CheckApply phase of execution. This is accomplished by calling the
|
|
`obj.init.Refresh() bool` method, and inspecting the return value. This is only
|
|
necessary if you plan to perform a refresh action. Refresh actions should still
|
|
respect the `apply` variable, and no system changes should be made if it is
|
|
`false`. Refresh notifications are generated by any resource when an action is
|
|
applied by that resource and are transmitted through graph edges which have
|
|
enabled their propagation. Resources that currently perform some refresh action
|
|
include `svc`, `timer`, and `password`.
|
|
|
|
It is very important that you include the `traits.Refreshable` struct in your
|
|
resource. If you do not include this, then calling `obj.init.Refresh` may
|
|
trigger a panic. This is programmer error.
|
|
|
|
### Edgeable
|
|
|
|
Edgeable is a trait that allows your resource to automatically connect itself to
|
|
other resources that use this trait to add edge dependencies between the two. An
|
|
older blog post on this topic is
|
|
[available](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
|
|
|
|
After you've included this trait, you'll need to implement two methods on your
|
|
resource.
|
|
|
|
#### UIDs
|
|
|
|
```golang
|
|
UIDs() []engine.ResUID
|
|
```
|
|
|
|
The `UIDs` method returns a list of `ResUID` interfaces that represent the
|
|
particular resource uniquely. This is used with the AutoEdges API to determine
|
|
if another resource can match a dependency to this one.
|
|
|
|
#### AutoEdges
|
|
|
|
```golang
|
|
AutoEdges() (engine.AutoEdge, error)
|
|
```
|
|
|
|
This returns a struct that implements the `AutoEdge` interface. This struct
|
|
is used to match other resources that might be relevant dependencies for this
|
|
resource.
|
|
|
|
### Groupable
|
|
|
|
Groupable is a trait that can allow your resource automatically group itself to
|
|
other resources. Doing so can reduce the resource or runtime burden on the
|
|
engine, and improve performance in some scenarios. An older blog post on this
|
|
topic is
|
|
[available](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
|
|
|
|
### Sendable
|
|
|
|
Sendable is a trait that allows your resource to send values through the graph
|
|
edges to another resource. These values are produced during `CheckApply`. They
|
|
can be sent to any resource that has an appropriate parameter and that has the
|
|
`Recvable` trait. You can read more about this in the Send/Recv section below.
|
|
|
|
### Recvable
|
|
|
|
Recvable is a trait that allows your resource to receive values through the
|
|
graph edges from another resource. These values are consumed during the
|
|
`CheckApply` phase, and can be detected there as well. They can be received from
|
|
any resource that has an appropriate value and that has the `Sendable` trait.
|
|
You can read more about this in the Send/Recv section below.
|
|
|
|
### Exportable
|
|
|
|
Exportable allows a resource to tell the exporter what subset of its data it
|
|
wishes to export when that occurs. It is rare that you will need to use this.
|
|
|
|
## Resource Initialization
|
|
|
|
During the resource initialization in `Init`, the engine will pass in a struct
|
|
containing a bunch of data and methods. What follows is a description of each
|
|
one and how it is used.
|
|
|
|
### Program
|
|
|
|
Program is a string containing the name of the program. Very few resources need
|
|
this.
|
|
|
|
### Hostname
|
|
|
|
Hostname is the uuid for the host. It will be occasionally useful in some
|
|
resources. It is preferable if you can avoid depending on this. It is possible
|
|
that in the future this will be a channel which changes if the local hostname
|
|
changes.
|
|
|
|
### Running
|
|
|
|
Running must be called after your watches are all started and ready. It is only
|
|
called from within `Watch`. It is used to notify the engine that you're now
|
|
ready to detect changes.
|
|
|
|
### Event
|
|
|
|
Event sends an event notifying the engine of a possible state change. It is
|
|
only called from within `Watch`.
|
|
|
|
### Refresh
|
|
|
|
Refresh returns whether the resource received a notification. This flag can be
|
|
used to tell a `svc` to reload, or to perform some state change that wouldn't
|
|
otherwise be noticed by inspection alone. You must implement the `Refreshable`
|
|
trait for this to work. It is only called from within `CheckApply`.
|
|
|
|
### Send
|
|
|
|
Send exposes some variables you wish to send via the `Send/Recv` mechanism. You
|
|
must implement the `Sendable` trait for this to work. It is only called from
|
|
within `CheckApply`.
|
|
|
|
### Recv
|
|
|
|
Recv provides a map of variables which were sent to this resource via the
|
|
`Send/Recv` mechanism. You must implement the `Recvable` trait for this to work.
|
|
It is only called from within `CheckApply`.
|
|
|
|
### World
|
|
|
|
World provides a connection to the outside world. This is most often used for
|
|
communicating with the distributed database. It can be used in `Init`,
|
|
`CheckApply` and `Watch`. Use with discretion and understanding of the internals
|
|
if needed in `Cleanup`.
|
|
|
|
### VarDir
|
|
|
|
VarDir is a facility for local storage. It is used to return a path to a
|
|
directory which may be used for temporary storage. It should be cleaned up on
|
|
resource `Cleanup` if the resource would like to delete the contents. The
|
|
resource should not assume that the initial directory is empty, and it should be
|
|
cleaned on `Init` if that is a requirement.
|
|
|
|
### Debug
|
|
|
|
Debug signals whether we are running in debugging mode. In this case, we might
|
|
want to log additional messages.
|
|
|
|
### Logf
|
|
|
|
Logf is a logging facility which will correctly namespace any messages which you
|
|
wish to pass on. You should use this instead of the log package directly for
|
|
production quality resources.
|
|
|
|
## Further considerations
|
|
|
|
There is some additional information that any resource writer will need to know.
|
|
Each issue is listed separately below!
|
|
|
|
### Resource registration
|
|
|
|
All resources must be registered with the engine so that they can be found. This
|
|
also ensures they can be encoded and decoded. Make sure to include the following
|
|
code snippet for this to work.
|
|
|
|
```golang
|
|
func init() { // special golang method that runs once
|
|
// set your resource kind and struct here (the kind must be lower case)
|
|
engine.RegisterResource("foo", func() engine.Res { return &FooRes{} })
|
|
}
|
|
```
|
|
|
|
### YAML Unmarshalling
|
|
|
|
To support YAML unmarshalling for your resource, you must implement an
|
|
additional method. It is recommended if you want to use your resource with the
|
|
`puppet` compiler.
|
|
|
|
```golang
|
|
UnmarshalYAML(unmarshal func(interface{}) error) error // optional
|
|
```
|
|
|
|
This is optional, but recommended for any resource that will have a YAML
|
|
accessible struct. It is not required because to do so would mean that
|
|
third-party or custom resources (such as those someone writes to use with
|
|
`libmgmt`) would have to implement this needlessly.
|
|
|
|
The signature intentionally matches what is required to satisfy the `go-yaml`
|
|
[Unmarshaler](https://godoc.org/gopkg.in/yaml.v2#Unmarshaler) interface.
|
|
|
|
#### Example
|
|
|
|
```golang
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes FooRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*FooRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to FooRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = FooRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
```
|
|
|
|
## Send/Recv
|
|
|
|
In `mgmt` there is a novel concept called _Send/Recv_. For some background,
|
|
please read the [introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/).
|
|
When using this feature, the engine will automatically send the user specified
|
|
value to the intended destination without requiring much resource specific code.
|
|
Any time that one of the destination values is changed, the engine automatically
|
|
marks the resource state as `dirty`. To detect if a particular value was
|
|
received, and if it changed (during this invocation of `CheckApply`) from the
|
|
previous value, you can query the `obj.init.Recv()` method. It will contain a
|
|
`map` of all the keys which can be received on, and the value has a `Changed`
|
|
property which will indicate whether the value was updated on this particular
|
|
`CheckApply` invocation. The type of the sending key must match that of the
|
|
receiving one. This can _only_ be done inside of the `CheckApply` function!
|
|
|
|
```golang
|
|
// inside CheckApply, probably near the top
|
|
if val, exists := obj.init.Recv()["some_key"]; exists {
|
|
obj.init.Logf("the some_key param was sent to us from: %s.%s", val.Res, val.Key)
|
|
if val.Changed {
|
|
obj.init.Logf("the some_key param was just updated!")
|
|
// you may want to invalidate some local cache
|
|
}
|
|
}
|
|
```
|
|
|
|
A resource can send a value during CheckApply by running the `obj.init.Send()`
|
|
method. It must always send a value if (1) it's not erroring in CheckApply, and
|
|
(2) if the `obj.SendActive()` method inside of CheckApply returns true. It is
|
|
not harmful to run the Send method if CheckApply is going to error, or if
|
|
`obj.SendActive()` returns false, just unnecessary. In the `!apply` case where
|
|
we're running in "noop" mode, and where the state is not correct, then you
|
|
should still attempt to send a value, but it is a bit ambiguous which value to
|
|
send. This behaviour may be specified in the future, but at the moment it's
|
|
mostly inconsequential. At the moment, `obj.SendActive()` is disabled at compile
|
|
time, but can be enabled if you have a legitimate use-case for it.
|
|
|
|
```golang
|
|
// inside CheckApply, somewhere near the end usually
|
|
if err := obj.init.Send(&ExecSends{ // send the special data structure
|
|
Output: obj.output,
|
|
Stdout: obj.stdout,
|
|
Stderr: obj.stderr,
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
```
|
|
|
|
You must also implement the `Sends()` method which should return the above
|
|
sending struct with all of the fields containing their default or values. Please
|
|
note, that those fields must have their struct tags set appropriately.
|
|
|
|
### Safety
|
|
|
|
Lastly, please note that in order for a resource to send a useful value, even
|
|
when its state is already correct (it may have run earlier for example) then it
|
|
may require the implementation of CheckApply to cache a return value for later
|
|
use. Keep in mind that you should store this securely should there be a chance
|
|
that sensitive info is contained within, and that an untrusted user could put
|
|
malicious data in the cache if you are not careful. It's best to make sure the
|
|
users of your resource are aware of its implementation details here.
|
|
|
|
## Composite resources
|
|
|
|
Composite resources are resources which embed one or more existing resources.
|
|
This is useful to prevent code duplication in higher level resource scenarios.
|
|
The best example of this technique can be seen in the `nspawn` resource which
|
|
can be seen to partially embed a `svc` resource, but without its `Watch`.
|
|
Unfortunately no further documentation about this subject has been written. To
|
|
expand this section, please send a patch! Please contact us if you'd like to
|
|
work on a resource that uses this feature, or to add it to an existing one!
|
|
|
|
## Frequently asked questions
|
|
|
|
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
|
respond by commit with the answer.)
|
|
|
|
### Can I write resources in a different language?
|
|
|
|
Currently `golang` is the only supported language for built-in resources. We
|
|
might consider allowing external resources to be imported in the future. This
|
|
will likely require a language that can expose a C-like API, such as `python` or
|
|
`ruby`. Custom `golang` resources are already possible when using mgmt as a lib.
|
|
Higher level resource collections will be possible once the `mgmt` DSL is ready.
|
|
|
|
### Why does the resource API have `CheckApply` instead of two separate methods?
|
|
|
|
In an early version we actually had both "parts" as separate methods, namely:
|
|
`StateOK` (Check) and `Apply`, but the [decision](https://github.com/purpleidea/mgmt/commit/58f41eddd9c06b183f889f15d7c97af81b0331cc)
|
|
was made to merge the two into a single method. There are two reasons for this:
|
|
|
|
1. Many situations would involve the engine running both `Check` and `Apply`. If
|
|
the resource needed to share some state (for efficiency purposes) between the
|
|
two calls, this is much more difficult. A common example is that a resource
|
|
might want to open a connection to `dbus` or `http` to do resource state testing
|
|
and applying. If the methods are combined, there's no need to open and close
|
|
them twice. A counter argument might be that you could open the connection in
|
|
`Init`, and close it in `Cleanup`, however you might not want that open for the
|
|
full lifetime of the resource if you only change state occasionally.
|
|
2. Suppose you came up with a really good reason why you wanted the two methods
|
|
to be separate. It turns out that the current `CheckApply` can wrap this easily.
|
|
It would look approximately like this:
|
|
|
|
```golang
|
|
func (obj *FooRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// my private split implementation of check and apply
|
|
if c, err := obj.check(ctx); err != nil {
|
|
return false, err // we errored
|
|
} else if c {
|
|
return true, nil // state was good!
|
|
}
|
|
|
|
if !apply {
|
|
return false, nil // state needs fixing, but apply is false
|
|
}
|
|
|
|
err := obj.apply(ctx) // errors if failure or unable to apply
|
|
|
|
return false, err // always return false, with an optional error
|
|
}
|
|
```
|
|
|
|
Feel free to use this pattern if you're convinced it's necessary. Alternatively,
|
|
if you think I got the `Res` API wrong and you have an improvement, please let
|
|
us know!
|
|
|
|
### Why do resources have both a `Cmp` method and an `IFF` (on the UID) method?
|
|
|
|
The `Cmp()` methods are for determining if two resources are effectively the
|
|
same, which is used to make graph change delta's efficient. This is when we want
|
|
to change from the current running graph to a new graph, but preserve the common
|
|
vertices. Since we want to make this process efficient, we only update the parts
|
|
that are different, and leave everything else alone. This `Cmp()` method can
|
|
tell us if two resources are the same. In case it is not obvious, `cmp` is an
|
|
abbrev. for compare.
|
|
|
|
The `IFF()` method is part of the whole UID system, which is for discerning if a
|
|
resource meets the requirements another expects for an automatic edge. This is
|
|
because the automatic edge system assumes a unified UID pattern to test for
|
|
equality. In the future it might be helpful or sane to merge the two similar
|
|
comparison functions although for now they are separate because they are
|
|
actually answer different questions.
|
|
|
|
### What new resource primitives need writing?
|
|
|
|
There are still many ideas for new resources that haven't been written yet. If
|
|
you'd like to contribute one, please contact us and tell us about your idea!
|
|
|
|
### Is the resource API stable? Does it ever change?
|
|
|
|
Since we are pre 1.0, the resource API is not guaranteed to be stable, however
|
|
it is not expected to change significantly. The last major change kept the
|
|
core functionality nearly identical, simplified the implementation of all the
|
|
resources, and took about five to ten minutes to port each resource to the new
|
|
API. The fundamental logic and behaviour behind the resource API has not changed
|
|
since it was initially introduced.
|
|
|
|
### Where can I find more information about mgmt?
|
|
|
|
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
|
|
|
## Suggestions
|
|
|
|
If you have any ideas for API changes or other improvements to resource writing,
|
|
please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in
|
|
order to get it right!
|