Intro
I recently found myself in need of a dnsmasq component and while writing it up decided to document my process as sort of a tutorial.
This post was written for the soon-to-be-release 0.5.0 version of Materia, though most if not all of it would also apply to v0.4.x .
We’re going to use dockurr’s container for this, but really dnsmasq is simple enough to package up and deploy that probably any container would work with it.
You can see the resulting component here and even use it in your own repositories, though at your own risk of course.
Creating the directory layout and manifest
First off, let’s pop into my materia repository to create the main component directory and manifest. I like to create the manifest first since a component is never valid without one.
We’ll run all these commands from the root of my repository.
> cd code/materia_repo
> ls
components secrets MANIFEST.toml mise.local.toml mise.toml
(The mise files are mostly just tasks for easy sops encryption)
Let’s create the folder and manifest file for the dnsmasq component:
> mkdir components/dnsmasq
> touch components/dnsmasq/MANIFEST.toml
> cd components/dnsmasq
> ls
MANIFEST.toml
I like to fill in the manifest last when writing components, so we’ll write up the container file first.
Writing the .container file
We’ll create a dnsmasq.container file first:
components/dnsmasq/dnsmasq.container:
[Container]
Image=docker.io/dockurr/dnsmasq:latest
ContainerName=dnsmasq
PublishPort=53:53/udp
ReloadSignal=SIGHUP
Volume=/etc/dnsmasq:/etc/dnsmasq
AddCapability=NET_ADMIN
AutoUpdate=registry
[Install]
# Needed to make container start at boot
WantedBy=multi-user.target default.target
This is essentially the simplest quadlet we could write: it will create the dnsmasq container and mount the host’s /etc/dnasmasq directory in it. If materia was run with this component right now, the only thing it would do is install this container file as-is to /etc/containers/systemd.
Realistically though we’ll want more than this. We want to supply configuration files as well as the container Quadlet and allow customization in case we need to run it on hosts with differing needs.
We probably also want to have a nice description and restart automatically if it fails.
Let’s convert it into a template and make it more flexible. Replace the dnsmasq.container file with this dnsmasq.container.gotmpl file:
components/dnsmasq/dnsmasq.container.gotmpl:
[Unit]
Description=dnsmasq container
StartLimitIntervalSec=400
StartLimitBurst=3
[Service]
Restart=on-failure
RestartSec=5s
[Container]
Image=docker.io/dockurr/dnsmasq:{{ .containerTag }}
ContainerName=dnsmasq
PublishPort={{ .hostPort }}:53/udp
ReloadSignal=SIGHUP
Volume={{ m_dataDir "dnsmasq" }}/dnsmasq.conf:/etc/dnsmasq.conf:Z
{{- if .useSeperateFiles }}
Volume={{ m_dataDir "dnsmasq" }}/hosts:/etc/hosts:Z
Volume={{ m_dataDir "dnsmasq" }}/ethers:/etc/ethers:Z
{{- end }}
AddCapability=NET_ADMIN
AutoUpdate=registry
[Install]
WantedBy=multi-user.target default.target
See the differences? Here’s a brief summary of what changed:
We added
[Unit]and[Service]sections. Like the[Install]section these are normal systemd unit properties that will apply to the generated unit.We added two simple variables:
{ .containerTag }and{ .hostPort }. These will be templated in with values from your configured attributes vaults, or from thedefaultssection of the component manifest. Spoilers: We’re going to set them in the component manifest.We added an if-statement with
{- if .useSeperateFiles }; since these are normal Go Templates we can use any feature available there. I’m old fashioned and like having dnsmasq use/etc/hostsand the oft-forgotten/etc/ethersfiles, but some situations or users might want just the onednsmasq.conffile.We used the
m_dataDirmacro; these are built-in functions for materia that provide useful features. In this case them_dataDir "component_name"macro resolves to data directory for thecomponent_name, letting us refer to data files in a component without hardcoding references. In this case, it would be/var/lib/materia/components/dnsmasq/.Finally, we used
:Zto get aroundselinuxpermissions issues; feel free to skip this step if you don’t use selinux. If you don’t include it you may see errors in the logs about not being able to access the data files.
Now we have a single container file that’s more robust and flexible thanks to systemd and templating. Since there’s no other quadlet’s needed here (i.e. we’re not creating any podman volumes or networks) we’ll move to the data files.
Adding data files
We need to create three data files: dnsmasq.conf, hosts, and ethers. If this component was being designed as a remote component we would make this part with more robust templating, but since that would be an article it’s own right we’re going to be a little lazy and create the following three files as such:
components/dnsmasq/dnsmasq.conf.gotmpl:
{{ .confContents }}
components/dnsmasq/hosts.gotmpl:
{{ .hosts }}
components/dnsmasq/ethers.gotmpl:
{{ .ethers }}
These files will be filled with the contents of their respective attributes.
This setup gives us maximum flexibility with minimum blog post length :)
Writing the component manifest
This brings us to the final file, the component manifest. We need to do two things here: set the default values for all the variables we used and configure what services should be running.
components/dnsmasq/MANIFEST.toml:
[Defaults]
containerTag = "latest"
hostPort = "53"
useSeperateFiles = true
confContents = ""
hosts = ""
ethers = ""
[[Services]]
Service = "dnsmasq.container"
ReloadedBy = ["hosts","ethers","dnsmasq.conf"]
We’ve created two sections here: [Defaults] and [[Services]].
[Defaults]is a table that defines the default attribute values for this component. For example, thecontainerTagattribute will be templated as “latest” by default, unless overridden somewhere else in your repository (usually as a host attribute). You usually always want to set a default value for attributes, unless you specifically do not want the component to be useable without the user setting one. We’re going to set everything for dnsmasq this time, but an argument can be made for not settingconfContentsto force the user to provide a configuration.[[Services]]is an array of service definitions. Materia (unless configured otherwise) starts any systemd units or quadlets defined here as part of the component installation and checks to make sure they’re running whenever it updates. Here we defined the maindnsmasqservice by telling it to start thednsmasq.containerquadlet (which it will translate to the proper systemd unit). We also defined thednsmasq.containerservice to be reloaded by thehosts,ethers, anddnsmasq.confresources. That means whenever any of those files are updated, materia will trigger asystemctl reloadfor the dnsmasq service.
The resulting file layout for the component should look like this:
> ls components/dnsmasq
dnsmasq.conf.gotmpl dnsmasq.container.gotmpl ethers.gotmpl hosts.gotmpl MANIFEST.toml
That’s it! The dnsmasq component is now complete and ready for use.
Adding it to a host
Just to demonstrate, we’re going to assign this component to my server named viscous. We’ll do this by editing the main MANIFEST.toml file at the root of the repository and adding it to the host config for viscous.
MANIFEST.toml:
[hosts.viscous]
components = ["dnsmasq"]
And that’s all there is to it. The next time materia runs on viscous it will automatically install and template the dnsmasq.container,dnsmasq.conf, hosts, and ethers files onto the host and start the dnsmasq container.