Shrink binaries
ci: Call "go mod download" before running "make check"
ci: Rename .build to .builds
A configuration management system for Linux and BSDs, focused on configuring the local host, and reaching out to other hosts if it needs information from them.
Where govern differs from things like Salt, Ansible, Puppet, Chef, etc., is that its core is written in a compiled language (Go). The core, itself, does nothing more than collect facts, see which runners are available, and translate state files before passing them to the runners.
Facts are details about a managed host.
Facts are kept in a directory structure, where each file represents the fact's name, and the directories act as a hierarchy for the facts.
Fact files may be executable. When govern examines the mode of a fact file, if the file is executable, govern will execute it using the data written to STDOUT as the fact value.
If a fact file is not executable, govern will simply read the file and use its contents as the fact value.
nesv: Should facts be limited in size? If yes, what should the size limit be?
Leading and trailing whitespace is stripped from the contents of a fact value.
govern-provided facts are typically installed to /usr/local/lib/govern/facts.d
.
Additional facts may be written to /usr/local/etc/govern/facts.d
.
By default, govern will look at this directory and prioritize the facts in
this directory over those in /usr/local/lib/govern/facts.d
.
For example, govern provides the os/name
fact.
If you create a fact with the same hierarchy in /usr/local/etc/govern/facts.d
:
echo "My Awesome OS" > /usr/local/etc/govern/facts.d/os/name
the next time you run govern facts
,
you will see the os/name
fact is set to My Awesome OS
.
Adding an executable fact is not much different than adding a plain-text fact.
Let's create a new fact, os/kernel/version
, that returns the current version
of our operating system's kernel.
$ sudo mkdir -p /usr/local/etc/govern/facts.d/os/kernel
$ cat > /usr/local/etc/govern/facts.d/os/kernel/version <<EOF
#!/bin/sh
uname -r
EOF
$ chmod 0755 /usr/local/etc/govern/facts.d/os/kernel/version
Now run govern facts
(output has been snipped for brevity):
$ govern facts
...
os/kernel/version 5.11.2-arch1-1
...
There are no restrictions on which language an executable fact is written in. All that matters is that it is executable. Be careful.
If you wish to disable a fact from being read, you may remove the read-bits from the file's mode.
chmod a-r path/to/fact
This will work for both regular facts, as well as executable facts. Simply removing the execute-bits from an executable fact's mode will result in govern simply reading the contents of the file into a fact. For cleanliness, to disable an executable fact you should also remove its execute bits:
chmod -rx path/to/fact
Since facts are organized using a directory hierarchy, you cannot have both a rule file and directory with the same name.
For example, it might seem logical to have the following fact structure:
os OpenBSD
os/kernel/version 6.8
However, for os/kernel/version
to exist, os
needs to be a directory.
This is why the fact for the current operating system's name is os/name
,
and not os
.
The final element of a fact's name can be guaranteed to be a file, while all
leading elements can be guaranteed to be a directory.
State files describe the state of a resource.
State files are written in HCL, and bear a
structural resemblance to Salt's .sls
state files.
The following example describes a pkg
(package) resource that will ensure
the "emacs" package is installed:
pkg "emacs" {
state = "installed"
}
When govern reads this state file, it will execute the pkg
runner.
Resources are structured like so:
runner-name "resource-name" {
arg1=val1
...
argN=valN
}
runner-name
is the name of the runner govern should invoke to handle the
resource.
resource-name
is the name of the resource to pass to the runner.
Between the curly-braces are key=value
pairs.
Most key-value pairs are passed directly to a runner.
The following key-value pairs are not passed to a runner, as they are
used by govern to order the resources.
The following key-value pairs in a resource block may be specified to assist govern in determining an order to apply the resource:
Key | Value | Description |
---|---|---|
before | [RREF, ...] | Schedule this resource before the listed RREFs. |
after | [RREF, ...] | Schedule this resource after the listed RREFs. |
By default, govern will apply resources in the order they are defined.
Internally, govern attempts to order resources into a directed acyclic graph (DAG).
By using the before
and after
keys in a resource definition, the ordering
of the resources can be influenced.
When govern detects a loop within the resource ordering:
A -> B -> C -> A
it will terminate with an error indicating the detected loop, before any resources are applied.
An RREF (resource reference) is a string value used to identify a resource.
runner-name:resource-name
Resources may "notify" another resource when they are successfully applied. The successful application of a resource is determined by the exit status of a resource's runner.
Take the following resources for example:
pkg "nginx" {
state = "installed"
notify = ["service:nginx:restart"]
}
service "nginx" {
state = "running"
}
When the pkg
runner exits with a status code of 0 (zero),
and indicates applying the resource pkg:nginx
resulted in a change,
govern will "notify" the service:nginx
resource with the restart
signal.
Notification strings take the following format:
RREF:SIGNAL
In this example, the service
runner will be invoked like so:
GOVERN_SIGNAL=restart /usr/local/lib/govern/bin/service nginx
The name of the signal used to notify the resource is passed to the service
runner as an environment variable GOVERN_SIGNAL
.
NOTE(nesv): Should the environment variable name just be "SIGNAL"?
See Execution phases for details on when notifications are sent.
Runners are programs that govern will execute to perform a task specified in a state file.
govern will call the runner programs in /usr/local/lib/govern/bin
.
Runners may be written in any programming language.
Runner programs are expected to take positional arguments in key=value
format.
Take the following state file:
pkg "emacs" {
state = "installed"
}
govern will translate the pkg:emacs
resource into key=value
-formatted
strings, and pass them to the runner as positional arguments:
/usr/local/lib/govern/bin/pkg emacs state=installed
The resource name (in this example, "emacs"
) will always be the first
positional argument to the runner.
The key-value pairs in a resource definition are formatted as key=value
when passed as a positional argument to a runner.
Should a value contain whitespace characters,
the entire argument will be shell-quoted.
For example, given the following (fictional) resource:
stuff "/etc/fstab old" {
foo = "yes, no, maybe so"
...
}
Would invoke the stuff
runner like so:
/usr/local/lib/govern/bin/stuff "/etc/fstab old" "foo=yes, no, maybe so"
When a runner is executed, the following environment variables are set:
Variable | Description |
---|---|
GOVERN |
Contains the absolute path to the govern binary that was invoked. |
GOVERN_FACTS_PATH |
Colon-separated directories containing the paths to the facts directories govern was invoked with. |
Runners must check to see if the GOVERN_SIGNAL
environment variable is set.
If the environment variable is set, the runner must operate in a "notification" mode. In notification mode, runners must not apply/ensure any resources. They may only act on the given signal.
If a runner receives an unexpected signal value, the runner must write an error message to STDERR, and exit with a non-zero (0) status code.
To provide an example of a runner, written in POSIX sh(1), implementing proper GOVERN_SIGNAL handling:
#!/bin/sh
set -eu
if [ -n "${GOVERN_SIGNAL}" ]
then
# We are in "notification mode".
case "${GOVERN_SIGNAL}" in
restart)
# do stuff...
;;
reload)
# do some other stuff...
;;
*)
printf "unexpected signal: %s\n" "${GOVERN_SIGNAL}" >&2
esac
# Since we were in "notification mode", the rest of the runner should
# not execute, so we will exit here.
exit 0
fi
govern also maintains the responsibility of rendering templated files.
Templates may be added to /usr/local/etc/govern/templates.d
.
The templating engine used is Go's
text/template.
Additional templating functions have been added for convenience.
fact "os/name"
is replaced by the given fact name;hostfact "name-or-ipaddr" "os/name"
retrieves the fact os/name
from the host
specified by name-or-ipaddr
;yesno
takes a Boolean value, and renders yes
if the value is true
(no
, otherwise).You may render a template using the govern render
command.
This is mainly useful for checking to make sure your facts are propagating,
and checking your templates.
State files themselves are templates, that will be rendered by govern before being passed to a runner. This allows for state files to be shared across heterogeneous hosts; in other words, you should write your state files so that they could work on both Linux systems, and BSD systems.
This is also helpful in situations where, for example, the name of a package may change depending on the Linux distribution.
Building off of our pkg "emacs"
example, we can modify the state file to
support both Arch Linux and OpenBSD:
{{$os := fact "os/name"}}
{{$distro := fact "os/distribution"}}
{{$emacs := ""}}
{{if $os == "Linux" and $distro == "Arch Linux"}}
{{$emacs = "emacs"}}
{{else if $os == "OpenBSD"}}
{{$emacs = "emacs-27.1-no_x11"}}
{{end}}
pkg "{{$emacs}}" {
state = "installed"
}
govern has several phases of execution. Each subsection describes what govern is doing in each phase. The phases are described in the order they occur.
Facts are gathered from the local host, as well as any remote hosts.
Facts from the local host are always fetched serially. Facts from remote hosts are fetched in parallel. The number of remote hosts that are queried in parallel for their facts is configurable.
State files are treated as templates and renderedto a temporary location.
Resources from all rendered state files are collected, and organized into a directed acyclic graph.
If any cycles/loops are detected, govern will terminate with an error message.
Resources are applied in the order determined by the DAG.
Resources are "notified" based on the result of the Resource application phase.