A tiny tool for simplifying system provisioning with simple bash scripts.
[] (http://travis-ci.org/d11wtq/skittle)
Skittle makes it really easy to script install procedures, configure computers or do just about anything that takes multiple steps using bash.
Because Skittle aims for zero dependencies, it is not intended that you install it in your system, instead prefering that you just add it to your project directly. It is less than 200 lines long (including whitespace and function definitions) and lives in a single file.
curl -O https://raw.githubusercontent.com/d11wtq/skittle/master/bin/skittle
chmod +x skittle
If you do want to add it to your system, go ahead place it on your $PATH
.
There are very few concepts to learn when it comes to using Skittle. The first
concept is knowing that the skittle
executable takes a single argument, which
is the name of a dependency it should resolve.
The second concept, is that a dependency is just a function that defines two other functions: one to indicate if it needs to run, and another that specifies how to run it.
Finally, dependencies may specify other depdencies that they require to run (i.e. they are recursively defined).
Let's look at a simple example of creating a log directory for a fictonal service called "turtle" (It was just a name that came to mind!)
We'll run this dependency like so.
bash-3.2$ ./skittle log_dir
+ log_dir
|
\ [fail] log_dir
Error: Cannot find dependency 'log_dir'
As you might expect, this produces an error, because we haven't actually
specified what the log_dir
dependency should do. We need to create a file
named "./deps/log_dir.sh"
and add a small amount of code.
# ./deps/log_dir.sh
log_dir() {
dir_path=/var/log/turtle
is_met() {
ls $dir_path
}
meet() {
sudo mkdir $dir_path
}
}
Save the file and go ahead and run skittle log_dir
again. You should see
everything go green and an [ok]
indicator.
bash-3.2$ ./skittle log_dir
+ log_dir
|
\ [ok] log_dir
bash-3.2$
Note
sudo
may prompt for your password when you run this dep.
If you take a look , /var/log/turtle
should have been created.
Quite simply, Skittle looks for your depedency under ./deps
, using the
convention that it has a file name matching the dependency, but ending in
".sh".
Next, it evaluates the function—in a subshell—knowing that it produces is_met
and meet
functions.
If the is_met
function returns a zero exit status, nothing happens. If,
however it returns non-zero, it runs the meet
function which should cause
subsequent calls to is_met
to return zero. Following this pattern makes
Skittle dependencies idempotent. Go ahead and run it again. It still returns
ok.
Ok, so this example was a bit basic. Creating the directory alone is probably
not enough. Let's ensure that directory is writable only to a user named
'turtle'. We now have to do four things to satisfy log_dir
:
- Ensure the directory exists.
- Ensure the turtle user exists.
- Ensure the directory is owned by turtle.
- Ensure the directory has 755 permissions.
We'll use Skittle's require
function for this.
# ./deps/log_dir.sh
log_dir() {
dir_path=/var/log/turtle
require dir_exists
require dir_ownership
require dir_permissions
dir_exists() {
is_met() {
ls $dir_path
}
meet() {
sudo mkdir $dir_path
}
}
dir_ownership() {
turtle_user_exists() {
is_met() {
id turtle
}
meet() {
sudo useradd turtle
}
}
is_met() {
[[ `ls -ld $dir_path | awk '{print $3}'` = "turtle" ]]
}
meet() {
sudo chown -R turtle: $dir_path
}
require turtle_user_exists
}
dir_permissions() {
is_met() {
[[ `ls -ld $dir_path | awk '{print $1}'` = "drwxr-xr-x" ]]
}
meet() {
sudo chmod -R 0755 $dir_path
}
}
}
Now when we run skittle log_dir
, we see it executes a tree. Again, this is
idempotent—you can run it over and over just fine.
bash-3.2$ ./skittle log_dir
+ log_dir
|
+-+ dir_exists
| |
| \ [ok] dir_exists
|
+-+ dir_ownership
| |
| +-+ turtle_user_exists
| | |
| | \ [ok] turtle_user_exists
| |
| \ [ok] dir_ownership
|
+-+ dir_permissions
| |
| \ [ok] dir_permissions
|
\ [ok] log_dir
bash-3.2$
Notice that the original log_dir
code moved into an inner depedency called
dir_exists
, since it was only checking if the log directory existed. The
other requirements have then been stated using require
.
Notice also that dir_ownership
has a nested dependency turtle_user_exists
.
This is allowed with Skittle so that you can group related dependencies
together. In this case however, turtle_user_exists
probably doesn't belong
here, as it is not directly related to the log directory. It is very simple to
just move the function definition to ./deps/turtle_user_exists.sh
. Try it,
everything should work the same.
Sometimes it is useful for dependencies to accept arguments, so that they
become more general and re-usable. A great example of this is
turtle_user_exists
from our earlier example. The code here could work for any
user, so we can generalize it and accept a username as an argument. Because
dependencies are just bash functions, arguments are numbered $1
, $2
etc.
# ./deps/user_exists.sh
user_exists() {
username=$1
is_met() {
id $username
}
meet() {
sudo useradd $username
}
}
Now we can change log_dir
to use this generalized dep instead.
# ./deps/log_dir.sh
log_dir() {
dir_path=/var/log/turtle
# ... snip ...
dir_ownership() {
require user_exists turtle
is_met() {
[[ `ls -ld $dir_path | awk '{print $3}'` = "turtle" ]]
}
meet() {
sudo chown -R turtle: $dir_path
}
}
# ... snip ...
}
Note It is important to store the function arguments to variables so they can be used inside
is_met
andmeet
.
Sometimes you'll want to provide some arbitrary output to the console while
Skittle is processing the dependency tree. Using echo
won't work, since
Skittle gathers all output on stdout
and stderr
into the log file. Instead,
Skittle provides log
, which prints a message to the console and reflects
which branch of the dependency tree this log output comes from.
You can use log
anywhere inside the main function body of the dep, or
inside is_met
and meet
.
For example, in our generic user_exists
dep, it might be a good idea to show
which user the dep is being run for.
# ./deps/user_exists.sh
user_exists() {
username=$1
log "Checking user: $username"
is_met() {
id $username
}
meet() {
sudo useradd $username
}
}
This will output something along the lines of.
bash-3.2$ ./skittle log_dir
+ log_dir
|
+-+ dir_exists
| |
| \ [ok] dir_exists
|
+-+ dir_ownership
| |
| +-+ user_exists
| | |
| | \ [..] Checking user: turtle
| | |
| | \ [ok] user_exists
| |
| \ [ok] dir_ownership
|
+-+ dir_permissions
| |
| \ [ok] dir_permissions
|
\ [ok] log_dir
bash-3.2$
Another suggested use of log
is to provide feedback to the user during
long running operations.
compile_source() {
cd project_src/
is_met() {
[[ -f some_binary ]]
}
meet() {
log "Compiling (may take some time)"
make
}
}
Ok, so we can create a log directory for our turtle service just fine. You could go on to install the entire turtle service, by breaking the problem down into similarly small tasks and putting them all under a top level dependency, like so.
install_turtle() {
require copy_files
require runlevels
require log_dir
require data_dir
}
Running ./skittle install_turtle
will run each of the deps listed in order.
If any single dep fails, the entire install process will halt. It's not hard
to see how something like this could be wrapped under a simple install.sh
for
users of the turtle service to run.
That's pretty much all there is to it!
At some point you may want to break your dependencies down into separate
projects in order to allow for re-use in different ways. In general, this is as
simple as adding nested directories under ./deps, however, there is one caveat:
if your deps are coded using relative paths to support files, such as the
following, you may run into trouble nesting your deps, since $PWD
is relative
to the user running skittle
, not relative to the dependency file itself.
etc_issue() {
wanted=./deps/issue_file.txt
is_met() {
diff $wanted /etc/issue
}
meet() {
sudo cp -f $wanted /etc/issue
}
}
If this dep is nested at, say ./deps/common/deps/etc_issue.sh, it will not do what you expect due to the relative path.
For this purpose, Skittle provides $p
, which is implicitly set for each dep
being run. This special variable defines the directory from which the
dependency file itself was loaded. You should use it everywhere you mean
"relative to the current dependency".
etc_issue() {
wanted=$p/issue_file.txt
is_met() {
diff $wanted /etc/issue
}
meet() {
sudo cp -f $wanted /etc/issue
}
}
Now the above dep will run anywhere it is copied to.
Skittle uses Skittle to test itself. To run the tests, run skittle
with
the 'tests' dep.
./bin/skittle tests
Reading through the test code (in the deps directory) is good way to see an example of Skittle code too.
- Dependencies can be stored in subdirectories
- Other supported locations for
./deps
are~/skittle-deps
and./skittle-deps
- The dependency function is unset before its
is_met
andmeet
functions are run. This makes it safe to name a dependency the same as a system binary (i.e. if you execute the system binarygit
in a dep namedgit
, you will indeed execute /usr/bin/git, rather than recurse into the dep function)
I work on open source projects for free and because I genuinely enjoy giving to the community, but of course any donations are well-received. You can donate a small amount via gittip if you're feeling generous.
Copyright © 2014 Chris Corbyn. See the LICENSE file for details.