Git Recombine Pattern for /etc config
Recording /etc/
config in git is a recommended way to track history and revert
breaking changes. In many cases, content needs to be
imported and transformed from other repositories before being deployed as
/etc/
config on a VM . Config changes to /etc
are often made in haste
during development & in emergencies. While git is helpful in recording those
changes locally, often content needs to be managed on one machine and pushed to
another. Or content can be in an outside repo with a different directory
schema.
Git Recombine is a decentralized strategy to allow content to flow in any direction and manage merge conflicts when they occur. With Git Recombine, which relies on git clone, git subtree & git push — content can be edited anywhere — either in-place or on a separate machine — and deployed to /etc.
Git Recombine is not a replacement for ansible, terraform, chef or other VM management tools—it’s a content-first approach that compliments more formal VM management. Being content-based, it’s faster & more dynamic than writing templates and transforms. It catches merge conflicts early and immediately when the config is applied, avoiding clobbered changes. It tracks all changes and requires minimal work to record history.
Repository Layout
Git Recombine relies on pushing commits around multiple clones (local or remote).
- the primary repo is in
/etc
(/etc/.git
) $HOME/clones/etc-staging
is a clone of/etc/
for illustration. It could be any remote.- app-specific clones like
$HOME/clones/smokeping
,$HOME/clones/lighttpd
are external repositories with a different directory structure
Let’s go through an example setting up Smokeping & Lighttpd on alpine with Git Recombining 2 third-party repos, smokeping & lighttpd, and one etc-staging repo
Creating the Git Repo
Create the git repo with git init
, adding receive.denyCurrentBranch=updateInstead
to
support deploy-on-push, and status.showUntrackedFiles=no
to reduce clutter
cd /etc
sudo git init .
sudo chown -R $USER .git
# optional, reduces clutter
git config --add status.showUntrackedFiles no
# checkout (deploy) files to /etc upon push
git config receive.denyCurrentBranch updateInstead
# add your first directory as usal
git add smokeping
Cloning /etc and Pushing Around Content
Git Recombine relies on multiple clones. This way content can be managed within
/etc/
directly or edited in the etc-staging clone on a remote machine. git fetch
and git push
will deploy config to /etc
cd $HOME/clones
$ git clone /etc ./etc-staging
$ cd /etc
# make changes to any file e.g. /etc/lighttpd/lightttpd.conf
$ git commit -m "add alias to lighttpd.conf"
$ cd $HOME/etc-staging
# fetch all commits and branches from primary repo /etc/
$ git fetch origin
# reconcile changes
$ git rebase origin/master
Pushing from etc-staging
to /etc
And you can deploy to /etc
by pushing from $HOME/clones/etc-staging
. git will
block this push as usual if there are conflicts. use git hooks to further
restrict this as needed.
$ cd $HOME/clones/etc-staging
# edit ./lighttpd/lighttpd.conf
$ git commit -m "added hostname param"
# this will deploy to /etc/ because receive.denyCurrentBranch=updateInstead
$ git push origin HEAD
Recomposing config from an outside Repository
git subtree
is an underappreciated command that works to transform and
integrate content from outside repositories into a target repository by using a
merge commit. It’s similar to submodule
, except that all content and commits
are stored within the target repository. Because merge commits are used,
subtree pull
can reconcile future commits into an existing history.
Structure Before
clones
├── smokeping
| └── Config
├── etc-clone
├ └── smokeping
└── config.d
Let’s Recombine smokeping/Config
→ etc-staging/smokeping/config.d
Transform the config
git subtree split
transforms trees . In this case we are moving /Config/*
to
the top of the tree so we can merge it into our ~/clones/etc-staging
repo
$ cd ~/clones/smokeping
$ git subtree split --prefix=config \
--annotate='(smokeping-config)' \
--squash --rejoin \
--branch smokeping-config
Now content has been moved to the top of the tree
$ git checkout smokeping-config
git ls-tree HEAD|head -n 2
100644 blob cbcfc41a361bb548cd68445bee8c613c0b3f8936 Alerts
100644 blob 0ca91f6f49bc89acc1c8b96a2a4f170bb8533d6c Database
Recombine into etc-staging using subtree
git subtree add
will merge the config into a subtree (subdirectory) . In this case
~/clones/smokeping:/
→ ~/clones/etc-staging:smokeping/config.d
# merge content from ~/clones/smokeping into here as smokeping/config.d
# --squash is optional -- omit it if you prefer keeping the full source repo history
git subtree add --prefix=smokeping/config.d ~/clones/smokeping smokeping-config --squash
Review the merge log to see the squashed commit.
$ git log -1
23696e5 - Merge commit '920056da1b35aafe5e50280008d20c0c5b6fd20b' as 'smokeping/config.d' (8 hours ago) <Anthony Metzidis>
Conclusions and Next Steps
The goal of Git Recombine is to allow for content to flow in any direction— among /etc & a development workspace — among outside source repos, while allowing for transforms and ad-hoc commits. Since changes to /etc are often haphazard and urgent, having tools to manage many various sources of content — from within and outside /etc itself — will help improve change management and merge conflicts.
Most often, only git push or git subtree add/merge will be necessary, and overhead will be similar to your familiar git add-commit-push flow. With practice you’ll find it more manageable to push content around via git than to rsync or copy/paste.
More Tools & Resources
- etckeeper is a great tool to automate saving to etc nightly & during package installation
- git docs on subtree undersell subtree as an “advanced merge” feature. In fact — it’s better than submodule and enables many more flows when a transform is needed.