The evolution of a frecklet
This page describes the different ways to create a frecklet. To learn how more about how to call/use those, please check out this page.
To get into detail about all the properties and allowed keys/values of a full-blown frecklet, read the Anatomy of a frecklet page.
Supported formats
A frecklet is a text file in either yaml, json, or toml format (other formats might be supported later). It can also be, depending on the context (e.g. when using freckles as a Python library) a dict or list data structure. In most cases though it'll be a text file. In the following, we'll exclusively use 'yaml' as our data format.
The elastic (non-)schema
frecklets don't have to follow a 'fixed' schema; they can 'mature' as they become more important. The freckles parser allows for several ways to express the same thing so the complexity of your code can mirror the importance of the context it is executed in. This page lists the different ways to describe tasks items and their metadata within a frecklet, from the shortest, most minimal way to the most powerful and descriptive.
Offering such a 'non-schema' might be considered fragile and wrong by some (I don't think that to be the case, obviously -- I think it's a worthwhile experiment, and a good trade-off for the read-ability it enables, and the general flexibility it brings to the table). If you don't feel comfortable with this idea but still -- for some reason -- want to use freckles, I'd suggest to only use the most verbose way of writing frecklets (described at the bottom of this page). It still might make sense to go through all the other options, as it should make it easier to understand how everything works.
The list of tasks
Without any additional metadata, a minimal frecklet is just a list of strings and/or dicts.
A list of (one or more) strings
In it's most basic form, a frecklet is a text file that contains a list of strings where each string represents a command (a.k.a. other frecklet) that does not require any arguments.
Each list item needs to be the name of another frecklet that exists in the current context
(get a list of all possible ones with: freckles list
).
Here's an example:
- docker-service
Let's put that into a file called my-docker-install.frecklet
. Issuing:
> frecklecute my-docker-install.frecklet
A list of single-key dicts
In the example above we don't need any custom variables, as installing Docker is usually pretty straight-forward, and there is no configuration option that requires user input. This uses a list of single-key dicts, a data structure you'll see often within freckles as it's quite easy for a human to grasp what it is meant to express (esp. in 'yaml' format).
Let's say we want to create a new user, which -- obviously -- as a minimum requires us to provide a username. We'll use a
ready-made and available frecklet again (the user-exists
one), only this time with some custom
parameters (we can investigate the available argument names and what they do with either freckles frecklet args user-exists
or frecklecute user-exists --help
):
- user-exists: name: markus
or, if we also want to specify the users' UID:
- user-exists: name: markus uid: 1010
In both cases, after putting the content in a file (say, my-new-user.frecklet
), we can create
the user (or rather: make sure the user exists) with a simple:
frecklecute my-new-user.frecklet
Mixed string(s) and dicts
We can easily mix and match those two types:
- user-exists: name: markus - docker-service
Or, if we want to make sure the newly created user with a specific id is in the group
that is allowed to user 'docker', we could write (after checking the docker-service
documentation):
- user-exists: name: markus uid: 1010 - docker-service: users: - markus
Note: if we did not need the custom uid
for our user, the docker-service
user would have created the user automatically, and user-exists
would not have been necessary.
Single-and double-key dictionaries
The (single-key dicts) example from above can also be expressed as a list of dicts in a slightly different format:
- frecklet: user-exists vars: name: markus - frecklet: docker-service vars: users: - markus
This by itself is not useful, as it's just a more verbose, and less readable way of saying the same thing. It makes more
sense once we add another keyword though: target
(as in the --target
option of the frecklecute
command).
This enables us to have tasks that are executed on different targets, in the same frecklet. By default, a frecklet executes on the target
that is specified on the commandline with --target
, or, if that is not used, 'localhost'. Having 'target' in the frecklet
will override both options. Here's how that would look:
- frecklet: file-with-content vars: path: /tmp/install.log content: | Installed Docker on host 'dev.frkl.io'. - frecklet: docker-service target: [email protected] vars: users: - markus
This example is a bit nonsensical, but where this comes in really handy, for example, is when you want to provision a new VM from a cloud provider. The first task would be executed locally, and talk to the providers API to create a new VM. The second one would connect to that VM (probably as root), and does some initial setup (like provisioning an admin user, disabling password-login for ssh, etc.).
There is a further evolution step to double-key dictionary frecklet-items. This is only usable in advanced use-cases, so we'll ignore that for now, and come back to it later, at the end of the page. For now, le'ts look into metadata to improve our frecklets usability (and usefulness).
The metadata dictionary
frecklets like the ones we discussed so far are really quick to create, they are good for prototyping, and serve as easy-to-understand starting points for more complex tasks. Once you get to a stage where you want to share a frecklet, or maybe use it in production, I'd recommend adding some metadata though. There are different types of metadata you can add:
- Documentation (
doc
keyword) - Arguments (
args
keyword) - Generic metadata, to be used in plugins or for other purposes (
meta
keyword, which we'll ignore for now)
Once we want to add metadata, a frecklet becomes a dict-like data structure. The task-list we used so far moves to a key called frecklets
.
Adding documentation
Having documentation is always good, and the best place for documentation to live is very close to
the thing it is documenting. If we want to add documentation to a frecklet, we need to transform our
frecklet content into a dictionary, and move the current task-list (well, list of a single task in the example below)
under the frecklets
key:
frecklets: - user-exists: name: markus uid: 1010
doc: short_help: Creates the user 'markus' with the uid 1010. help: | Creates a single user, named 'markus'. The UID of this user will be '1010'. frecklets: - user-exists: name: markus uid: 1010
This information can be used by the freckles framework, and displayed where necessary. For example, the frecklecute
application can use it to construct a command-line help text:
> frecklecute my-create-user.frecklet --help Usage: frecklecute my-create-user.frecklet [OPTIONS] Creates a single user, named 'markus'. The UID of this user will be '1010'.
Adding arguments
Up until now, our frecklet is hardcoded to do exactly one thing, creating a user with a fixed name and UID. What if we want to re-use it with different values? This is a typical use-case for variables, and what arguments are used for in command-line tools.
Non-typed arguments
If you don't want to clutter your frecklet with metadata about its argument(s), and you are happy for them to be required and non-empty strings, all you have to do is use a special freckles template syntax ( {{:: key ::}}
) for the values you want user input for:
frecklets: - user-exists: name: "{{:: username ::}}" uid: 1010
This will tell freckles to convert the {{:: username ::}}
string into a (required) commandline option, and use the user input for it as the variable value:
> frecklecute my-create-user.frecklet --help Usage: frecklecute create-user.frecklet [OPTIONS] n/a Options: --username TEXT n/a [required] --help Show this message and exit. $ frecklecute my-create-user.frecklet Usage: frecklecute my-create-user.frecklet [OPTIONS] Error: Missing option "--username".
This is how we run this minimal frecklet now:
> frecklecute --ask-sudo-pass my-create-user.frecklet --username admin SUDO PASS: ╭╼ starting run │ ├╼ running frecklet: /home/markus/my-create-user.frecklet (on: localhost) │ │ ├╼ starting Ansible run │ │ │ ├╼ remove cached sudo credential │ │ │ │ ╰╼ ok │ │ │ ├╼ ensure user 'admin' exists │ │ │ │ ╰╼ ok │ │ │ ╰╼ ok │ │ ╰╼ ok │ ╰╼ ok ╰╼ ok
Maybe we also want to ask for the UID? Sure:
frecklets: - user-exists: name: "{{:: username ::}}" uid: "{{:: uid ::}}"
Execute frecklecute my-create-user --help
again to see the newly created cli help.
Typed arguments
Using non-typed arguments is a quick and easy way to create frecklets that take user input, and it's well suited for simple task-lists you want to create quickly. It keeps the content of the frecklet neat and tidy, and you can see instantly what it is supposed to do.
For more involved frecklets it is recommended to specify (and document) your arguments though. Similar to how the doc
key works, every frecklet can also have an args key. Here's an example for our my-create-user.frecklet
:
args: username: type: string required: yes empty: no doc: short_help: the name of the new user uid: type: integer required: no doc: short_help: the uid of the new user frecklets: - [user-exists](https://freckles.sh): name: "{{:: username ::}}" uid: "{{:: uid ::}}"
Every variable we want to ask the user needs to be present as key under the args
section, and also at least once somewhere in frecklets
. If the former is not the case, freckles will use a default argument spec (a required item, non-empty). If the latter is not the case, freckles will just ignore it.
Note:
Internally, freckles uses the Cerberus and Click Python libraries to validate the arguments, as well as create the command-line interface for frecklecute
. The configuration under the args
key is forwarded more or less unchanged to those libraries, so please peruse their respective documentation for details if necessary (for now, at least).
Notice how we use required: no
for our uid
value. This is a good way to specify optional arguments. If a 'none' value or empty string is passed to a key in a dict, it won't be forwarded to the child frecklet that is called. Also, we have specified the type of the argument as an integer under args
. This causes the variable to be validated, and if successful, converted into the proper type.
Let's see what frecklecute
makes of this:
$ frecklecute my-create-user.frecklet --help Usage: frecklecute my-create-user.frecklet [OPTIONS] n/a Options: --username TEXT the name of the new user [required] --uid INTEGER the uid of the new user --help Show this message and exit.
What happens if we provide a non-integer value for uid
? Let's see:
$ frecklecute my-create-user.frecklet --username markus --uid markus Usage: frecklecute my-create-user.frecklet [OPTIONS] Try "frecklecute my-create-user.frecklet --help" for help. Error: Invalid value for "--uid": markus is not a valid integer
And that's basically it. There are more details you can adjust, both in terms how the frecklet presents itself to its users, and in terms of specifying exactly which tasks to execute, and in which manner. For more details on those, please refer to the freckles documentation.
Exploded frecklet
-items
As I've mentioned before, there is an additional evolutionary step to how the items in the list under frecklets
can
be expressed. This is the internal representation of such an item within freckles, and it offers the most flexibility,
but trades in some readability and ease of use. We'll only give a broad overview of the topic here, for more in-detail
information please refer to the Anatomy of a frecklet page.
For the purpose of explaining this, we'll use a frecklet without metadata, and only one task. This format is really only useful to develop new frecklets that call low-level tasks that don't have their own frecklet yet. Ideally, end-users would only ever have to deal with pre-developed frecklets, but once requirements become more complex, chances increase that some custom development needs to be done.
Ok, here's the frecklet we'll be working with:
# filename: example.frecklet - user-exists: name: markus
Very simple, one task, makes sure a user exists on a system. You can use freckles to display the fully-exploded, internally used data structure of a frecklet. Here's how:
> freckles frecklet explode example.frecklet doc: {} args: {} frecklets: - frecklet: name: user-exists type: frecklet task: command: user-exists vars: name: markus
The important part is the one list item under the frecklets
key. We can see the item is a directory with 3 keys:
frecklet
: contains general metadata about the frecklet item and it's typetask
: contains adapter-specific metadata (in this case that does not really apply, as the item is just another frecklet)vars
: the vars to use for this frecklet in this run
As I've said, using this format just to call an existing frecklet does not make too much sense. Let's see how the user-exists
frecklet is implemented internally. Apart from creating the users group if it does not exist and some optional metadata (which we'll both ignore here), this is the basic implementation of user-exists
:
frecklets: - frecklet: name: user type: ansible-module desc: short: "ensure user '{{:: name ::}}' exists" task: become: true vars: name: "{{:: name ::}}" state: present groups: "{{:: group ::}}" append: true uid: "{{:: uid ::}}" system: "{{:: system_user ::}}" password: "{{:: password | sha512_crypt ::}}" shell: "{{:: shell ::}}"
You could put this into a file and call it with frecklecute <filename> --help
, and you'd get a basic help message, similar to the one we saw above, with all of the arguments being required (and strings).
The 'vars' value works like in any of the other examples we've looked at so far, so I'll not go into that again here. The interesting stuff happens in frecklet
, and task
.
The frecklet
(sub-)key
The important key in this part of the configuration is type
. This lets freckles know which one of the available freckles adapters to use to process this item. Every adapter registers with freckles with a list of supported types. In this case (ansible-module
) the nsbl one will be used.
There are other keys you can put into frecklet
, the most important one being skip
, which lets you skip a task in certain situations. Here we are also showing desc
, which contains the message the user sees when this task is executed.
The task
(sub)-key
This one lets you fine-tune the behaviour of this (dynamically created) frecklet in question. In this case, the user
Ansible module will be called with the become
key set to true
.
The content of this sub-key is highly dependent on the adapter used, so you'll have to refer to the documentation of the adapter in question for details.
That's all folks. Check out the other docs, or head over to the friends of freckles if you have questions!