<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[ChrisCruft]]></title><description><![CDATA[Things I find floating around in my head]]></description><link>https://blog.hapgood.com/</link><image><url>https://blog.hapgood.com/favicon.png</url><title>ChrisCruft</title><link>https://blog.hapgood.com/</link></image><generator>Ghost 4.1</generator><lastBuildDate>Fri, 20 Mar 2026 15:21:52 GMT</lastBuildDate><atom:link href="https://blog.hapgood.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Clojure Zippers - Part One]]></title><description><![CDATA[<!--kg-card-begin: markdown--><p>I started working on a replacement for the existing clojure.zip library with a couple of enhancements to support navigation by index or key.  I went down a rabbit hole, finding things I didn&apos;t like about the existing implementation.  Of course the more I learned the more I</p>]]></description><link>https://blog.hapgood.com/clojure-zippers-part-one/</link><guid isPermaLink="false">605618e95d722e0001d85a93</guid><category><![CDATA[Clojure]]></category><category><![CDATA[zipper]]></category><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Tue, 06 Apr 2021 17:54:00 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><p>I started working on a replacement for the existing clojure.zip library with a couple of enhancements to support navigation by index or key.  I went down a rabbit hole, finding things I didn&apos;t like about the existing implementation.  Of course the more I learned the more I came to appreciate  that it reflected good compromises.  Still, Clojure has evolved since Rich Hickey wrote the original implementation and at the end of the day, I still wanted to make some changes.</p>
<p>This is the first of a three-part series.</p>
<p>Part One: <a href="blog.hapgood.com/clojure-zippers-part-one/">Zipper Primer</a><br>
Part Two: Clojure Implementation<br>
Part Three: Extensions</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><h2 id="zipperprimer">Zipper Primer</h2>
<p>Like Hickey, my starting point was the <a href="https://www.st.cs.uni-saarland.de//edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf">Functional Pearl</a> paper published by G&#xE9;rard Huet in 1997.  I pored over Huet&apos;s paper until my eyes hurt -I have a barely detectable ability to read OCaml which probably handicapped me more than I would have liked to admit.  But after much experimenting at the REPL I think I finally understand the Zipper. To help others in their quest to understand the Zipper, I&apos;ve generated some diagrams focusing on some critical concepts.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p>This diagram shows a simple tree along with a path from the Root to a terminal leaf &quot;element&quot; (I avoid the term &quot;node&quot; because, as we&apos;ll see below, it has a different meaning in the context of Huet&apos;s zipper).</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.hapgood.com/content/images/2021/03/Zipper-Basic.svg" class="kg-image" alt="An example tree and a path to a leaf element" loading="lazy" width="317" height="531"><figcaption>A Simple Tree</figcaption></figure><!--kg-card-begin: markdown--><p>An element can be both contained within its parent and contain other elements. The green in the diagram below is the same value: at the third level it is contained within its parent alongside its siblings.  At the fourth level we see that it is also a collection containing sub-elements in its own right.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.hapgood.com/content/images/2021/03/Zipper-Tree-Duality.svg" class="kg-image" alt="A diagram showing how an element is both contained within its parent and contains its children" loading="lazy" width="317" height="531"><figcaption>Duality - Contained &amp; Containing</figcaption></figure><!--kg-card-begin: markdown--><p>This next diagram gets to the crux of the zipper.  It shows the hierarchy of <code>Node</code> structures corresponding to the path shown above.  Each <code>Node</code> is a three-tuple comprised of a list of left (Huet calls them &quot;elder&quot;) siblings, a pointer to its parent <code>Node</code> and a list of right (&quot;younger&quot;) siblings.  There are five <code>Node</code>s in this diagram (the purple <code>Node</code> at the top has a nil value in place of a pointer to its parent).  It&apos;s essential to understand that the <code>Node</code> does not track the value between the left siblings and the right siblings.  There is essentially a gap between the left and right.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.hapgood.com/content/images/2021/03/Zipper-Linked-Nodes.svg" class="kg-image" alt="A diagram showing the recursively linked Nodes that represent a path to the root" loading="lazy" width="329" height="457"><figcaption>Recursively linked Nodes</figcaption></figure><!--kg-card-begin: markdown--><p>If you squint just right, the above image evokes a zipper.  Imagine recursively pulling each arrow up to the gap between its parent&apos;s left and right siblings.  If you had the missing &quot;gap&quot; element from the bottom-most (blue) node, you could reconstitute the corresponding element. You could in turn pull the reconstituted blue element up to the red node above it and thus reconstitute a red element, and with that element you could... and so on up to the root to reconstitute the entire tree.</p>
<p>The Zipper uses the <code>Loc</code> data structure to explicitly track this bottom-most missing gap element.  A <code>Loc</code> is a tuple of the path to the root (a linked list of <code>Node</code>s as depicted above) plus the single element missing from the bottom <code>Node</code>.  Below in green are the two components of a <code>Loc</code> for the eighth sibling of the third descendant of the example tree.  The dark green is the gap element itself (frequently named <code>t</code> for &quot;tree&quot; in Huet&apos;s paper) while the light green is the recursive Node (frequently named <code>p</code> for &quot;path&quot; in Huet&apos;s paper).  Keep in mind that p <strong>is</strong> the entire path to the root -I&apos;ve only shaded the first <code>Node</code>, but the arrow pointing up should remind you that the entire path to the root is available recursively through ancestor <code>Node</code>s.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.hapgood.com/content/images/2021/03/Zipper-Loc.svg" class="kg-image" alt="A diagram showing the two components of a Loc" loading="lazy" width="323" height="531"><figcaption>The Loc</figcaption></figure><!--kg-card-begin: markdown--><h3 id="whyzippers">Why Zippers?</h3>
<p>What advantages do Zippers represent compared to alternative data structures?</p>
<p>An obvious advantage, compared to the raw tree, is that the Zipper represents the entire (possibly updated) tree <em>and</em> a current position (or focus).  Said another way, Zippers are a purely functional way of navigating, modifying and pointing within an arbitrary tree.</p>
<p>Another notable advantage is the efficient editing of immutable data structures<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>.  For immutable data, the Zipper&apos;s recursive <code>Node</code> structure tracks the minimal set of elements that need to be re-written to effect any change. With no loss in expressivness, the necessary re-writing is deferred until &quot;zipping&quot; up the data structure; multiple changes are thus  effected all at once, potentially minimizing expensive write operations.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>for  mutable data a change to a sub-elements can be effected simply by overwriting it; expanding/inserting and contracting/deleting are also relatively simple <a href="#fnref1" class="footnote-backref">&#x21A9;&#xFE0E;</a></p>
</li>
</ol>
</section>
<!--kg-card-end: markdown--><p></p>]]></content:encoded></item><item><title><![CDATA[Docker Compose Bastion Host]]></title><description><![CDATA[<p>Ensure basic connectivity between containers (common network, known names).</p><p>A good starting point for a bastion host image is: <code>binlab/bastion:latest</code> &#xA0;But it requires a file volume that is my public key. &#xA0;That works fine locally, but won&apos;t work when the container is deployed to</p>]]></description><link>https://blog.hapgood.com/docker-compose-bastion-host/</link><guid isPermaLink="false">605618e95d722e0001d85a8c</guid><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Sun, 28 Feb 2021 21:45:53 GMT</pubDate><content:encoded><![CDATA[<p>Ensure basic connectivity between containers (common network, known names).</p><p>A good starting point for a bastion host image is: <code>binlab/bastion:latest</code> &#xA0;But it requires a file volume that is my public key. &#xA0;That works fine locally, but won&apos;t work when the container is deployed to AWS or some other docker host. &#xA0;The solution is to build a new image with my public key baked in. &#xA0;Here&apos;s the <code>Dockerfile</code>:</p><figure class="kg-card kg-code-card"><pre><code class="language-Dockerfile">FROM binlab/bastion

LABEL maintainer=&quot;cch1@hapgood.com&quot;

ARG USER=bastion
ARG GROUP=bastion
ARG HOME=/var/lib/bastion

COPY ./authorized_keys ${HOME}/authorized_keys

RUN chown ${USER}:${GROUP} ${HOME}/authorized_keys
RUN chmod 600 ${HOME}/authorized_keys</code></pre><figcaption>Dockerfile to build a personalized bastion</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-Dockerfile">docker run --name bastion --hostname bastion -p 22222:22/tcp -v bastion:/usr/etc/ssh:rw -e &quot;PUBKEY_AUTHENTICATION=true&quot; -e &quot;GATEWAY_PORTS=false&quot; -e &quot;PERMIT_TUNNEL=false&quot; -e &quot;X11_FORWARDING=false&quot; -e &quot;TCP_FORWARDING=true&quot; -e &quot;AGENT_FORWARDING=true&quot; binlab/bastion:latest</code></pre><figcaption>Start the bastion server without authorized keys</figcaption></figure><p>Reference: <a href="https://www.techrepublic.com/article/how-to-create-your-own-docker-image/">https://www.techrepublic.com/article/how-to-create-your-own-docker-image/</a></p>]]></content:encoded></item><item><title><![CDATA[Clojure Zippers for Maps]]></title><description><![CDATA[<p>There is a wonderful <a href="https://clojure.github.io/clojure/clojure.zip-api.html">library</a> in Clojure for traversing and manipulating hierarchical data structures using <a href="https://en.wikipedia.org/wiki/Zipper_(data_structure)">zippers</a>. &#xA0;The internet even has several good <a href="http://josf.info/blog/2014/03/28/clojure-zippers-structure-editing-with-your-mind/">references</a> on how understand the mind-warping nature of zippers and use them effectively.</p><p>My goal was to navigate a nested structure of maps, like this:</p><!--kg-card-begin: markdown--><p><code>{:a0 {:b0</code></p>]]></description><link>https://blog.hapgood.com/clojure-zippers-for-maps/</link><guid isPermaLink="false">605618e95d722e0001d85a92</guid><category><![CDATA[Clojure]]></category><category><![CDATA[zipper]]></category><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Wed, 10 Feb 2021 15:07:58 GMT</pubDate><content:encoded><![CDATA[<p>There is a wonderful <a href="https://clojure.github.io/clojure/clojure.zip-api.html">library</a> in Clojure for traversing and manipulating hierarchical data structures using <a href="https://en.wikipedia.org/wiki/Zipper_(data_structure)">zippers</a>. &#xA0;The internet even has several good <a href="http://josf.info/blog/2014/03/28/clojure-zippers-structure-editing-with-your-mind/">references</a> on how understand the mind-warping nature of zippers and use them effectively.</p><p>My goal was to navigate a nested structure of maps, like this:</p><!--kg-card-begin: markdown--><p><code>{:a0 {:b0 {:c0 1 :c1 {}} :b1 {:d0 2}} :a1 {:z {}}}</code></p>
<!--kg-card-end: markdown--><p>After a couple of iterations I settled on this for my zipper:</p><pre><code class="language-Clojure">(defn map-zip
  [m]
  (let [root (clojure.lang.MapEntry. ::root m)]
    (z/zipper (comp map? val)
              (comp seq val)
              (fn [[k children] children&apos;] (clojure.lang.MapEntry. k (into (empty children) children&apos;)))
              root)))</code></pre><!--kg-card-begin: markdown--><p>This zipper works perfectly for iteratively traversing and editing my data structure. For example, one task was to prune the empty sub-maps (like the entry at <code>(get-in m [:a1 :z])</code>.  Here&apos;s a function that performs that task:</p>
<pre><code class="language-Clojure">(defn prune
  &quot;Recursively prune the empty submap leaves from the nested map structure `m`&quot;
  [m]
  (let [z (map-zip m)]
    (val (loop [z z]
           (if (z/end? z)
             (z/root z)
             (recur (if (and (z/branch? z) (not (seq (z/children z))))
                      (z/remove z)
                      (z/next z))))))))
</code></pre>
<!--kg-card-end: markdown--><p>But despite the effectiveness, in several cases I found the implementation counter-intuitive -specifically when navigating. &#xA0; The fundamental problem is that navigating using relative motion (left, right, leftmost, rightmost) work well for sequences (a common use case for zippers) but is inefficient when I already have an index associating keys to children for my maps.</p><p>I decided to create a new library inspired by the Clojure zipper implementation but leveraging the efficient and idiomatic navigation by sequences of keys (&#xE0; la <code>get-in</code> , <code>update-in</code>, <code>assoc-in</code>). &#xA0; &#xA0;Projected completion date: not now.</p>]]></content:encoded></item><item><title><![CDATA[Who's in charge here: Versions from SCM]]></title><description><![CDATA[<p><em>I originally wrote entry for the Roomkey tech blog in 2016. &#xA0;That site has been shut down due to the Coronavirus-triggered demise of Roomkey in 2020. &#xA0;I&apos;m reproducing it here, un-edited, for archival purposes.</em></p><p>This article examines some of the issues involved in managing versions and</p>]]></description><link>https://blog.hapgood.com/whos-in-charge-here-versions-from-scm/</link><guid isPermaLink="false">605618e95d722e0001d85a91</guid><category><![CDATA[Clojure]]></category><category><![CDATA[git]]></category><category><![CDATA[lein-v]]></category><category><![CDATA[scm]]></category><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Sun, 03 Jan 2021 16:11:09 GMT</pubDate><content:encoded><![CDATA[<p><em>I originally wrote entry for the Roomkey tech blog in 2016. &#xA0;That site has been shut down due to the Coronavirus-triggered demise of Roomkey in 2020. &#xA0;I&apos;m reproducing it here, un-edited, for archival purposes.</em></p><p>This article examines some of the issues involved in managing versions and describes an alternative to the default management offered by leiningen that we use at Room Key. &#xA0;It assumes familiarity with <a href="http://clojure.org/">Clojure</a>, <a href="https://github.com/technomancy/leiningen">leiningen</a> and <a href="https://git-scm.com/">git</a>.</p><h2 id="some-history">Some History</h2><p>In 2010 Room Key<sup><a>[1]</a></sup> started developing a web service using Clojure 1.2.0 and Leiningen 1.x. &#xA0;By early 2011, we had decided that the version of our artifacts, particularly the primary web site application, needed to be something more flexible than commited text within <code>project.clj</code>. &#xA0;In February, we wrote the first leingingen support code that slurped the version from git and supplied it to the project as it was being evaluated (and it was indeed evaluated back in the Leiningen 1.x days). &#xA0;It wasn&apos;t much code -only a couple of dozen lines (and the author was allergic to line lengths beyond about 40 characters). &#xA0;But it introduced a way of thinking that seems very natural and obvious. &#xA0;Since 2011, we have expanded on the original work in several steps and today, in mid-2016 the evolution of that code is <a href="https://github.com/roomkey/lein-v">lein-v</a>, a leiningen plugin that solves a lot of hairy issues surrounding versions.</p><h2 id="our-problem">Our Problem</h2><p>A version identifies software artifacts as they evolve over time. &#xA0;As such, versioning is a critical component of change management.</p><p>We find that leiningen&apos;s approach of having the version stored in the commited <code>project.clj</code> source requires either ambiguous versions (where many commits share the same version)<sup><a>[2]</a></sup> or unweildy commit practices (changing the version in <code>project.clj</code> on every commit).</p><p>We believe:</p><ol><li>Unique (and reproducible/committed) source should produce unique versions</li><li>Versioning should be painless in the simplest cases, but complex cases should still be manageable</li><li>Versions should reflect an ordering of source code where possible<sup><a>[3]</a></sup></li><li>Versioning information should live in the SCM repo -the source of source code truth</li><li>Version information is metadata and should not be stored within with the data it describes</li></ol><h2 id="a-solution">A Solution</h2><p>Lein-v uses git metadata to build a unique, reproducible and meaningful version for every commit. &#xA0;Along the way, it adds useful metadata to your project and artifacts (jar and war files) to tie them back to a specific commit. &#xA0;Consequently, it helps ensure that you never release an irreproduceable artifact.</p><h3 id="read-git-to-generate-a-version">Read git to generate a version</h3><p>Instead of reading a static string in <code>project.clj</code>, lein-v reads git tag and commit data to construct a version that is unique to the current commit. &#xA0;It then injects this git-derived version into all subsequent leiningen tasks. &#xA0;The version is constructed from:</p><ul><li>Coded tags of the form <code>v&lt;version&gt;</code> (generated by the release process)</li><li>SHA of the current commit</li><li>Commit distance from most recent version tag to the current commit</li></ul><p>The commit distance provides an ordering of commits and the SHA uniquely identifies commits. &#xA0;Together, they can be used to produce versions that don&apos;t rely on manual editing of <code>project.clj</code>.</p><h3 id="update-versions-logically-during-release">Update versions logically during release</h3><p>By leveraging leiningen&apos;s built-in support for extending its release process, lein-v can close the loop on version management by updating the version stored in git tags. &#xA0;Here is an example process that we use at Room Key:</p><pre><code class="language-clojure">    :release-tasks [[&quot;vcs&quot; &quot;assert-committed&quot;]
                    [&quot;v&quot; &quot;update&quot;] ;; compute new version &amp; tag it
                    [&quot;vcs&quot; &quot;push&quot;]
                    [&quot;deploy&quot;]]
</code></pre><p>The lein-v <code>update</code> task computes a new version from the current version and a command line bump parameter such as <code>:major</code> or <code>:alpha</code><sup><a>[4]</a></sup>. &#xA0;The new version is injected into the standard leiningen processing, which allows the leingingen <code>vcs</code> task to work as expected.</p><h2 id="a-note-on-version-structure">A note on version structure</h2><p>Since the dawn of software management (in all its various guises), version structure has been a contentious subject. &#xA0;Lein-v attempts to remain neutral on the subject of what a version should look like. &#xA0;But because leiningen ultimately relies on <a href="https://maven.apache.org/">maven</a> to resolve dependencies, lein-v leans towards maven&apos;s (loose) view of versions. &#xA0;Version structure is abstracted into two protocols (<code>SCMHosted</code> and <code>Releasable</code>), and implementations for <a href="http://maven.apache.org/ref/3.2.5/maven-artifact/">maven 3</a> and <a href="http://semver.org/">semver 2.0.0</a> are provided. &#xA0;It&apos;s not hard to write your own implementation and select it instead of the default <code>MavenVersion</code>.</p><h2 id="availability">Availability</h2><p>Lein-v is available on <a href="https://clojars.org/com.roomkey/lein-v">Clojars</a> and source is available on <a href="https://github.com/roomkey/lein-v">GitHub</a>. &#xA0;Pull requests are welcome and <a href="https://github.com/roomkey/lein-v/issues">issues</a> can be raised against our GitHub project as well.</p><h3 id="related-reading">Related Reading</h3><ul><li>(<a href="http://maven.apache.org/ref/3.2.5/maven-artifact/apidocs/org/apache/maven/artifact/versioning/ComparableVersion.html">http://maven.apache.org/ref/3.2.5/maven-artifact/apidocs/org/apache/maven/artifact/versioning/ComparableVersion.html</a>)</li><li>(<a href="http://www.sonatype.com/books/mvnref-book/reference/pom-relationships-sect-pom-syntax.html">http://www.sonatype.com/books/mvnref-book/reference/pom-relationships-sect-pom-syntax.html</a>)</li><li>(<a href="http://javamoods.blogspot.com/2010/10/world-of-versioning.html">http://javamoods.blogspot.com/2010/10/world-of-versioning.html</a>)</li><li>(<a href="http://download.eclipse.org/aether/aether-core/1.0.1/apidocs/org/eclipse/aether/util/version/GenericVersionScheme.html">http://download.eclipse.org/aether/aether-core/1.0.1/apidocs/org/eclipse/aether/util/version/GenericVersionScheme.html</a>)</li><li>(<a href="http://git.eclipse.org/c/aether/aether-core.git/tree/aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java">http://git.eclipse.org/c/aether/aether-core.git/tree/aether-util/src/main/java/org/eclipse/aether/util/version/GenericVersion.java</a>)</li><li>(<a href="http://books.sonatype.com/mvnref-book/reference/pom-relationships-sect-pom-syntax.html#pom-reationships-sect-versions">http://books.sonatype.com/mvnref-book/reference/pom-relationships-sect-pom-syntax.html#pom-reationships-sect-versions</a>)</li><li>(<a href="http://mojo.codehaus.org/versions-maven-plugin/version-rules.html">http://mojo.codehaus.org/versions-maven-plugin/version-rules.html</a>)</li><li>(<a href="http://maven.40175.n5.nabble.com/How-to-use-alternative-version-numbering-scheme-td123806.html">http://maven.40175.n5.nabble.com/How-to-use-alternative-version-numbering-scheme-td123806.html</a>)</li><li>(<a href="http://maven.apache.org/ref/3.2.5/maven-artifact/index.html">http://maven.apache.org/ref/3.2.5/maven-artifact/index.html</a>)</li><li>(<a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning">https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning</a>)</li><li>(<a href="http://docs.codehaus.org/display/MAVEN/Dependency+Mediation+and+Conflict+Resolution">http://docs.codehaus.org/display/MAVEN/Dependency+Mediation+and+Conflict+Resolution</a>)</li><li>(<a href="http://dev.clojure.org/display/doc/Maven+Settings+and+Repositories">http://dev.clojure.org/display/doc/Maven+Settings+and+Repositories</a>)</li><li>(<a href="http://maven.40175.n5.nabble.com/How-to-use-SNAPSHOT-feature-together-with-BETA-qualifier-td73263.html">http://maven.40175.n5.nabble.com/How-to-use-SNAPSHOT-feature-together-with-BETA-qualifier-td73263.html</a>)</li></ul><h3 id="notes">Notes</h3><hr><p>then known as Hotelicopter. <a>&#x21A9;&#xFE0E;</a></p><p>SNAPSHOT versions are a prime example of ambiguous versions, and we do not use them at Room Key. <a>&#x21A9;&#xFE0E;</a></p><p>branches in the source code repo make a total ordering impossible. <a>&#x21A9;&#xFE0E;</a></p><p>The actual bump parameters supported depend on the specific versioning format you use. &#xA0;The default maven format supports all the built-in leingingen bump levels (<code>:major</code> <code>:minor</code> <code>:patch</code> <code>:alpha</code> <code>:beta</code> and <code>:rc</code>) as well as <code>:snapshot</code> and <code>:release</code> to explicitly manage SNAPSHOT releases. <a>&#x21A9;&#xFE0E;</a></p>]]></content:encoded></item><item><title><![CDATA[Continuously Effecting the Product]]></title><description><![CDATA[<p><em>I originally wrote entry for the Roomkey tech blog in 2016. &#xA0;That site has been shutdown due to the Coronavirus-triggered demise of Roomkey in 2020. &#xA0;I&apos;m reproducing it here, lightly edited, for archival purposes.</em></p><p>&quot;There have been N days with no workplace accident&quot;</p><p>This</p>]]></description><link>https://blog.hapgood.com/continuously-effecting-the-product/</link><guid isPermaLink="false">605618e95d722e0001d85a90</guid><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Sun, 03 Jan 2021 16:08:13 GMT</pubDate><content:encoded><![CDATA[<p><em>I originally wrote entry for the Roomkey tech blog in 2016. &#xA0;That site has been shutdown due to the Coronavirus-triggered demise of Roomkey in 2020. &#xA0;I&apos;m reproducing it here, lightly edited, for archival purposes.</em></p><p>&quot;There have been N days with no workplace accident&quot;</p><p>This article examines an initiative at Room Key to lower the latency between identifying a desired change to code and deploying code to production. &#xA0;It assumes some familiarity with <a href="https://git-scm.com/">git</a>, Jenkins and Amazon Web Services.</p><h2 id="some-history">Some History</h2><p>Room Key&apos;s primary product is a <a href="http://www.roomkey.com">web site</a> delivered as a single page Javascript application that queries an AWS-hosted Clojure web service. &#xA0;We use two primary git repositories: one for the front-end app and one for the back-end webservice. &#xA0;From our formation in 2010 until very recently we practiced a form of agile development with a three week sprint. &#xA0;Once per sprint we would cycle through the same plan-code-stage-qa-deploy process.</p><h3 id="plan">Plan</h3><p>During planning, desired features were broken down into feature stories and priority bug fixes were scheduled with bug stories. &#xA0;The stories were written or adjusted in such a way as to be achievable within the upcoming sprint. &#xA0;The team agreed to a three-week plan which was communicated to external stakeholders.</p><h3 id="code">Code</h3><p>Once the plan was laid out in the form of stories for the current sprint, developers coded the features and bug fixes. &#xA0;Feature branches in our git repo were used to isolate complex stories from the master branch. &#xA0;For commits to the master branch, a Jenkins continuous integration server ran unit tests for both of the primary repositories.</p><h3 id="stage">Stage</h3><p>Towards the end of the second week of the sprint, developers would wrap up their development and merge their commits into the master branch. &#xA0;A front-end release candidate was identified and staged into the back-end repository as an asset. &#xA0;A back-end release candidate was then staged to a qa server.</p><h3 id="qa">QA</h3><p>Once the release candidate was staged, the QA team had a budget of roughly four days to perform regression tests and feature tests. &#xA0;Most of the feature tests were performed manually based on the acceptance criteria outlined in the feature stories. &#xA0;Regression tests were partially automated and worked in the browser.</p><h3 id="deploy">Deploy</h3><p>Once QA signed off on the release candidate, we deployed the artifacts to AWS by adjusting a CloudFormation template.</p><h2 id="problems">Problems</h2><p>Our approach was appealing in many ways: it was easy to explain; it provided regular feedback; compared to old-school waterfall planning methodologies, it was efficient; &#xA0;it ticked a lot of the &quot;agile&quot; boxes; and it promised the ability to update to our product every three weeks. &#xA0;But it had some painful failure modes that were exposed all too often because our reality is messy.</p><p>Our reality was that:</p><ol><li>Bugs from the previous sprint ate up wildly varying amounts of our time each sprint.</li><li>Stories changed in scope, and new high-priority stories were added after the sprint started.</li><li>Available resources changed due to sickness or other personal reasons.</li><li>The QA team found bugs of wildly varying severity in the new features.</li><li>Deployment issues occasionally arose.</li></ol><p>If these problems were encountered early in the sprint, we could usually recover. &#xA0;Problems uncovered late in the sprint had more severe repercussions. &#xA0;Obviously, there was less time to recover without missing the deployment schedule. &#xA0;But we also found that the further into the development cycle a failure appeared, the more likely it was to prevent <em>every</em> in-progress story from progressing. &#xA0;Why? &#xA0;Because we coupled together all stories and commits in a sprint... &#xA0;in essence, we willingly donned a straitjacket.</p><p>This coupling phenomenon where one story, or even one commit, can torpedo every story in a sprint can be explained by this observation:</p><p>Planning has as a primary goal the decomposition of high-level deliverables so that developers can work independently. &#xA0;For QA testing of the deliverables, the independent pieces need to be reconstituted into a recognizable feature. &#xA0;For deployment, all scheduled features need to be reconstituted into a single artifact.</p><hr><p><em>This is where the original draft blog post ended. &#xA0;Shortly after I drafted it, I became the CTO of Roomkey and addressed the issues above by adopting a continuous deployment model. &#xA0;It was not easy to get from &quot;here&quot; to &quot;there&quot; but the payoff was worth it. &#xA0;We parallelized and decoupled stories, invested in automated testing to find problem early and changed our processes fundamentally. &#xA0;Our approach probably will not work for most companies.</em></p><p><em>The biggest challenge was getting developers to buy into their responsibility for finding bugs early. &#xA0;Most developers accept responsibility for the bugs that they introduce. &#xA0;Fewer, but still many, accept responsibility for proactively finding bugs in the code for which they are responsible. &#xA0;Too few accept the collective responsibility of finding bugs (before delivery to QA) resulting from the integration of their code with the code of other developers. &#xA0;At Roomkey, this last challenge was a struggle. &#xA0;In a mature code base with many authors, I believe that a robust test suite is one of the best ways to reduce the number of regressions.</em></p><hr>]]></content:encoded></item><item><title><![CDATA[Use a Docker macvlan network to enhance the Unifi Controller on Synology]]></title><description><![CDATA[Running the UniFi Controller docker image with macvlan networking solves problems.]]></description><link>https://blog.hapgood.com/use-a-docker-macvlan-network-to-enhance-the-unifi-controller-on-synology/</link><guid isPermaLink="false">605618e95d722e0001d85a8a</guid><category><![CDATA[docker]]></category><category><![CDATA[synology]]></category><category><![CDATA[unifi]]></category><dc:creator><![CDATA[Christopher C Hapgood]]></dc:creator><pubDate>Fri, 03 Jul 2020 15:49:00 GMT</pubDate><media:content url="https://blog.hapgood.com/content/images/2020/11/usgs-KAsF7SBCmpk-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<h2 id="overview">Overview</h2><img src="https://blog.hapgood.com/content/images/2020/11/usgs-KAsF7SBCmpk-unsplash.jpg" alt="Use a Docker macvlan network to enhance the Unifi Controller on Synology"><p>By enabling the <code>macvlan</code> driver for Docker you can have each container appear as a layer two (Ethernet) device on your network. &#xA0;This provides extreme network isolation between containers and between containers and the host. &#xA0;Given the broad range of ports<sup><a>[1]</a></sup> required to be exposed and the attraction of Layer 2 adoption of new devices, it&apos;s particularly helpful to leverage <code>macvlan</code> for the UniFi controller container.</p><p>Note that the <code>macvlan</code> driver must be connected to a physical network adapter that supports promiscuous mode. &#xA0;The Synology RS1619xs+ Ethernet adapters meet this requirement as do most modern adapters. &#xA0;Note also that <code>macvlan</code> support is limtied to Linux hosts<sup><a>[2]</a></sup> -Synology NASs qualify.</p><h2 id="step-one-create-the-docker-network">Step One - Create the docker network</h2><p>Unfortunately it&apos;s not possible to create the <code>macvlan</code> network from the Synology GUI so we resort to using the command line. &#xA0;Here we create a docker virtual LAN to which containers can be attached and which is bridged to the physical network of the host:</p><pre><code>docker network create -d macvlan --subnet=192.168.1.0/24 --gateway=192.168.1.1 --ip-range=192.168.1.160/27 --aux-address &apos;host=192.168.1.160&apos; --opt parent=bond0 mac0
</code></pre><p>(These commands need to be run as root. &#xA0;<code>sudo</code> is one approach.)</p><p>Explanation of parameters:</p><ul><li><code>-d macvlan</code> : create a network using the <code>macvlan</code> driver.</li><li><code>--subnet=192.168.1.0/24</code> : define the physical layer 3 network in front of the bridge.</li><li><code>--gateway=192.168.1.1</code> : define the appropriate gateway for the physical network in front of the bridge.</li><li><code>--ip-range=192.168.1.160/27</code> : define the range of (layer 3) IP addresses that will be found behind the macvlan bridge on the virtual LAN. &#xA0;This effectively provides a pool of addresses that might be assigned dynamically to attached containers by the docker host<sup><a>[3]</a></sup>, or statically via parameters when launching containers. &#xA0;You might find <a href="http://www.subnet-calculator.com/">this tool</a> useful for selecting the pool.</li><li><code>--aux-address &apos;host=192.168.1.160&apos;</code> : reserve an IP address from the pool. &#xA0;Later in this tutorial the aux-address is used to provide an interface on the virtual LAN for the docker host itself.</li><li><code>--opt parent=bond0</code> : define the physical interface of the host that will be bridged to the virtual LAN. &#xA0;In this example we&apos;re using a bonded interface but in the simplest scenario <code>eth0</code> is a likely choice.</li><li><code>mac0</code> : name the docker network</li></ul><p>At this point the Docker network is created and you can see it in the GUI. &#xA0;You can also see the created network:</p><pre><code class="language-bash">docker network ls
</code></pre><p>References:</p><ul><li><a href="https://docs.docker.com/network/">Docker Network Docs</a></li><li><a href="https://docs.docker.com/network/macvlan/">Docker <code>macvlan</code> Docs</a></li><li><a href="https://docs.docker.com/network/network-tutorial-macvlan/">Docker Network Tutorial</a></li></ul><h2 id="step-two-create-an-interface-on-the-host-connecting-to-the-virtual-lan">Step Two - Create an interface on the host connecting to the virtual LAN</h2><p>The default bridging enabled by the <code>macvlan</code> driver does not create the additional infrastructure required to allow the host to communicate with the virtual LAN. &#xA0;Logic would imply that if the host can communicate with the physical network (192.168.1.0/24 in the example) and containers attached to the virtual LAN (192.168.1.176/27 in the example) can also communicate with the physical network (that&apos;s the whole point of this exercise) then the host itself should be able to communicate wih containers attached to the virtual LAN. &#xA0;But that behavior is not supported out of the box. &#xA0;Here we create the infrastructure to plug the gap. &#xA0;These four commands need to be run once, ideally during boot up of the host:</p><pre><code>ip link add macvlan0 address 7a:4f:97:de:cc:cc link bond0 type macvlan mode bridge
</code></pre><p>This creates a virtual interface device <code>macvlan0</code> subordinate to the &quot;physical&quot; interface <code>bond0</code>. &#xA0;At this point the MAC address is assigned to the virtual interface (see it with <code>ip link show macvlan0</code>) and we hard-code it to avoid having it change at every reboot.</p><pre><code>ip addr add 192.168.1.160/32 dev macvlan0
</code></pre><p>This assigns a static IP address to the virtual interface device <code>macvlan0</code> (see it with <code>ip addr show macvlan0</code>).</p><pre><code>ip link set macvlan0 up
</code></pre><p>This brings the <code>macvlan0</code> virtual interface device up. &#xA0;You should be able to ping the interface from the host: <code>ping 192.168.1.160</code></p><pre><code>ip route add 192.168.1.160/27 dev macvlan0
</code></pre><p>This creates a route to the virtual LAN (192.168.1.160/27) via the virtual interface device <code>macvlan0</code> (see it with <code>ip route show</code>).</p><p>These commands are not permanent. &#xA0;To make them permanent on the Synology NAS, create a &quot;Triggered Task&quot; that executes a &quot;User-defined script&quot; as root in response to the &quot;Boot-up&quot; event. &#xA0;The task (a bash script) should look something like this:</p><pre><code class="language-bash">while ! ip link show bond0 | grep -q &#x2018;state UP&#x2019;; do
sleep 1
done
ip link add macvlan0 address 7a:4f:97:de:cc:cc link bond0 type macvlan mode bridge
ip addr add 192.168.1.160/32 dev macvlan0
ip link set macvlan0 up
ip route add 192.168.1.160/27 dev macvlan0
</code></pre><h2 id="step-three-create-configure-and-run-the-unifi-controller-container">Step Three - Create, Configure and Run the UniFi Controller container</h2><p>Now that the networking infrastructure is in place, we can bind the Docker container to our <code>mac0</code> network. &#xA0;You can do this from the Docker GUI since the <code>mac0</code> network is visible and available for binding, but there is a big limitation in doing it this way: the IP address assigned to the container by the docker host is not guaranteed to be consistent. &#xA0;Until Synology supports macvlan networks overtly the best solution is to create the container manually. &#xA0;There are serveral good candidates<sup><a>[4]</a></sup> but I&apos;ve settled on <code>jacobalberty/unifi</code> due to its freshness and configurability. &#xA0;We&apos;re going to exploit this configurability to tune the container for running as the sole &quot;tenant&quot; of its virtual LAN host.</p><p>After using the Synology GUI to pull the latest image, here we create a container:</p><pre><code class="language-bash">docker create --name=UniFiController --net=mac0 --ip=192.168.1.161 -it -e BIND_PRIV=true -e UNIFI_HTTP_PORT=80 -e UNIFI_HTTPS_PORT=443 -e TZ=America/New_York -e RUNAS_UID0=false -v /volume1/docker/Unifi:/unifi jacobalberty/unifi:latest
</code></pre><p>Explanation of parameters:</p><ul><li><code>--name UniFiController</code> : name the container.</li><li><code>--net=mac0</code> : bind the container to the virtual LAN.</li><li><code>--ip=192.168.1.161</code> : assign the IP address of the virtual interface device.</li><li><code>-it</code> : keep STDIN open and allocate a psuedo-TTY.</li><li><code>-e BIND_PRIV=true</code> : we need this special privilge to bind to a port less than 1024.</li><li><code>-e UNIFI_HTTP_PORT=80</code> : this image uses this var to configure the UniFi controller to control the HTTP port.</li><li><code>-e UNIFI_HTTPS_PORT=443</code> : this image uses this var to configure the UniFi controller to control the HTTPS port.</li><li><code>-e TZ=America/New_York</code> : set the timezone of the container process.</li><li><code>-e RUNAS_UID0=false</code> : observe the principal of least privilege and run as a normal user.</li><li><code>-v /docker/UniFi:/unifi</code> : bind mount the <code>/docker/UniFi</code> share folder to <code>/unifi</code> in the container (the default location).</li><li><code>jacobalberty/unifi:latest</code> : create the container from the latest version of the image.</li></ul><p>You can tweak some of these parameters from within the Synology Docker GUI. &#xA0;Notice, however, the lack of port mapping for docker: by using the <code>macvlan</code> network instead of host networking we avoid the whole class of port conflicts.</p><p>Finally, run your container. &#xA0;The GUI works just fine for this. &#xA0;You can now browse to the assigned IP address and <em>default</em> port (<code>https://192.168.1.161</code> in this example).</p><p>References:</p><ul><li><a href="https://docs.docker.com/engine/reference/commandline/run/">Docker create documentation</a></li><li><a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">List of TZ values</a></li></ul><h2 id="summary">Summary</h2><p>We&apos;ve created a <code>macvlan</code> virtual network and adapter for Docker, created a virtual interface on the Docker host connecting to the virtual network and launched/configured a Docker container to live on that virtual network. &#xA0;At this point, Docker containers gain a whole new axis of isolation from the host and the port conflicts and mapping hassle typically associated with host networking are gone.</p><h3 id="future-improvements-and-evolution">Future Improvements and Evolution</h3><p>There are several areas that could stand improvement:</p><ul><li>Create DNS entries for the docker containers (on me, easy to do)</li><li>Use an existing DHCP server to assign IP addresses to the docker containers. &#xA0;This would require a Bridge/Relay on the Docker host, or maybe a proxy of some sort. &#xA0;It would also require a change to the way that Docker assigns the IP address to the container. &#xA0;(on Docker)</li><li>Enable <code>macvlan</code> networking support directly in the Synology GUI. &#xA0;This would ease the friction in running containers and provide a single point of administration/monitoring. &#xA0;Currently, for example, there is no way in the GUI to see the IP address assigned to a container bound to the <code>macvlan</code> network. (on Synology)</li></ul><h3 id="epilogue">Epilogue</h3><p>There are permissions issues with Synology&apos;s Docker implementation which make creating the container <em>and creating the bind mount</em> from the command line problematic. &#xA0;I finally worked around this issue by creating a throw-away container that created the bind mount point with the GUI. &#xA0;Then I deleted that container and created my &quot;final&quot; container using the command line. &#xA0;So the GUI-created container was a kind of surrogate mother to give birth to the mount point...</p><h3 id="credits">Credits</h3><p>This blog post draws greatly from <a href="https://tech.ligthartnet.nl/pi-hole-in-docker-on-synology/">u&#x1D09;&#x28D;p&#x1DD; &#x26F;&#x250; &#x1D09;</a> and <a href="https://blog.oddbit.com/post/2018-03-12-using-docker-macvlan-networks/">The Odd Bit</a>.</p><hr><p>Per the <a href="https://help.ui.com/hc/en-us/articles/218506997-UniFi-Ports-Used">UniFi documentation</a>. <a>&#x21A9;&#xFE0E;</a></p><p>Per the <a href="https://docs.docker.com/network/network-tutorial-macvlan/#prerequisites">docker docs</a>. <a>&#x21A9;&#xFE0E;</a></p><p>Sadly, <a href="https://gist.github.com/nerdalert/3d2b891d41e0fa8d688c">a DHCP client is not (yet) officially supported</a>. <a>&#x21A9;&#xFE0E;</a></p><p>Consider also <a href="https://registry.hub.docker.com/r/linuxserver/unifi-controller/">linuxserver/unifi-controller</a>. <a>&#x21A9;&#xFE0E;</a></p>]]></content:encoded></item></channel></rss>