~nesv/govern

A configuration management tool.
Shrink binaries
ci: Call "go mod download" before running "make check"
ci: Rename .build to .builds

clone

read-only
https://git.sr.ht/~nesv/govern
read/write
git@git.sr.ht:~nesv/govern

You can also use your local clone with git send-email.

#govern

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.

#Core

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

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.

#Provided facts

govern-provided facts are typically installed to /usr/local/lib/govern/facts.d.

#Adding your own facts

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

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
...
	
#Here be dragons

There are no restrictions on which language an executable fact is written in. All that matters is that it is executable. Be careful.

#Disabling facts

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
	

#"Limitations" with facts

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

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.

#Resource structure

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.

#Resource ordering

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.

#Resource references (RREFs)

An RREF (resource reference) is a string value used to identify a resource.

runner-name:resource-name
	
#Notifying resources

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
	
  • RREF is a resource reference (described above);
  • SIGNAL is a simple string, whose meaning is determined by the runner.

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

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.

#Notifications

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

#Templates

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).

#Rendering a template

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 are 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"
}

#Execution phases

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.

#Fact gathering

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 file processing

State files are treated as templates and renderedto a temporary location.

#DAG creation

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.

#Resource application

Resources are applied in the order determined by the DAG.

#Notification

Resources are "notified" based on the result of the Resource application phase.