<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on PHP Boy Scout</title><link>https://blog-570662.gitlab.io/post/</link><description>Recent content in Posts on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Wed, 20 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/post/index.xml" rel="self" type="application/rss+xml"/><item><title>Two bugs that taught me the rules</title><link>https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/cover-two-bugs-that-taught-me-the-rules.png" alt="Featured image of post Two bugs that taught me the rules" /&gt;&lt;p&gt;Some bugs are interesting because they&amp;rsquo;re subtle. These two were interesting because they were the exact opposite&amp;hellip; in each case the tool had a hard rule I simply didn&amp;rsquo;t know about, and its error message couldn&amp;rsquo;t be bothered to tell me what that rule was. Both came out of building the infrastructure toolchain, both cost me a good deal more time than they had any right to, and both are the sort of thing that looks blindingly obvious the moment you know it and utterly baffling until you do.&lt;/p&gt;
&lt;p&gt;So here they are, written down, partly to save you the bother and partly so I don&amp;rsquo;t go and forget them myself.&lt;/p&gt;
&lt;h2 id="bug-one-the-rule-less-job-that-skips-your-merge-requests"&gt;Bug one: the rule-less job that skips your merge requests
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;cicd&lt;/code&gt; gate components, in their first cut, shipped with no &lt;code&gt;rules:&lt;/code&gt; block. They were dead simple jobs: lint, scan, validate. No conditions, because they should just always run. Obviously.&lt;/p&gt;
&lt;p&gt;They ran on branch pipelines. On merge requests, they didn&amp;rsquo;t run at all! The gates that were the entire point of the components were simply absent from the one place you&amp;rsquo;d most want to see them&amp;hellip; the merge request.&lt;/p&gt;
&lt;p&gt;The cause is a GitLab CI rule that&amp;rsquo;s remarkably easy to go years without ever learning: a job with no &lt;code&gt;rules:&lt;/code&gt; block runs only on branch and tag pipelines. It does not run on merge-request pipelines. So &amp;ldquo;no conditions&amp;rdquo; doesn&amp;rsquo;t mean &amp;ldquo;runs everywhere&amp;rdquo; at all. It means &amp;ldquo;runs everywhere except a merge request&amp;rdquo;, which is about the least intuitive default I can think of.&lt;/p&gt;
&lt;p&gt;The fix is faintly absurd, and that&amp;rsquo;s exactly what makes it stick. You add an &lt;em&gt;unconditional&lt;/em&gt; rule: &lt;code&gt;rules: [{ when: on_success }]&lt;/code&gt;. The content of that rule does precisely nothing. It always matches. What actually matters is that the job now &lt;em&gt;has&lt;/em&gt; a &lt;code&gt;rules:&lt;/code&gt; block at all, because merely having one is what makes a job eligible for merge-request pipelines. A rule whose content is meaningless, added solely so the block exists. That&amp;rsquo;s the fix. I&amp;rsquo;ll admit I stared at it for a moment.&lt;/p&gt;
&lt;h2 id="bug-two-the-import-block-that-only-works-at-the-root"&gt;Bug two: the import block that only works at the root
&lt;/h2&gt;&lt;p&gt;The second one came from &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;. The &lt;code&gt;account-hardening&lt;/code&gt; module needed to adopt a resource that already existed in the account, which is exactly what OpenTofu&amp;rsquo;s &lt;code&gt;import {}&lt;/code&gt; block is for. So an &lt;code&gt;import&lt;/code&gt; block went into the &lt;code&gt;account-hardening&lt;/code&gt; module, right next to the resource it was adopting. The natural home for it, surely.&lt;/p&gt;
&lt;p&gt;OpenTofu disagreed, and rejected it outright. The rule: an &lt;code&gt;import&lt;/code&gt; block is only allowed in the &lt;em&gt;root&lt;/em&gt; module. It can&amp;rsquo;t live inside a child module. A module that wants one of its own resources imported can&amp;rsquo;t declare that import itself&amp;hellip; the import has to be declared up at the root, and the root caller does the adopting.&lt;/p&gt;
&lt;p&gt;The fix was to take the &lt;code&gt;import&lt;/code&gt; block out of the module and document caller-side adoption instead. The module describes the resource, and the root configuration that calls the module is where the &lt;code&gt;import&lt;/code&gt; actually lives.&lt;/p&gt;
&lt;h2 id="the-shape-they-share"&gt;The shape they share
&lt;/h2&gt;&lt;p&gt;Two unrelated bugs, in two completely different tools, and the same shape sitting underneath both of them.&lt;/p&gt;
&lt;p&gt;In each case the tool has a hard structural rule. Where a block is allowed to live. What makes a job eligible for a particular kind of pipeline. And in each case the error told me the tool was unhappy without telling me &lt;em&gt;which&lt;/em&gt; rule I&amp;rsquo;d broken, so the obvious next move (debugging my own logic) was the wrong move entirely. There was nothing wrong with the logic. The thing was simply in a place the tool doesn&amp;rsquo;t allow, or missing a block the tool quietly insists on.&lt;/p&gt;
&lt;p&gt;The lasting lesson here isn&amp;rsquo;t the two specific rules, useful as they are to know. It&amp;rsquo;s the reflex. When something that should obviously work just doesn&amp;rsquo;t, and the error is unhelpful, stop debugging your logic and start suspecting a structural rule about &lt;em&gt;where&lt;/em&gt; something is allowed to be, or &lt;em&gt;whether&lt;/em&gt; a thing is eligible in the first place. GitLab CI and OpenTofu both have a handful of these, and you mostly learn them the hard way, by tripping over them. Knowing the shape of the category at least means the next one costs you an hour instead of a whole afternoon.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Two bugs from building the toolchain, one shape. A GitLab CI job with no &lt;code&gt;rules:&lt;/code&gt; block runs on branches and tags but silently not on merge requests, and the fix is an unconditional &lt;code&gt;rules:&lt;/code&gt; block whose content does nothing and whose mere existence is the entire point. An OpenTofu &lt;code&gt;import&lt;/code&gt; block gets rejected inside a child module, because imports are only legal at the root, so the caller adopts and the module just describes.&lt;/p&gt;
&lt;p&gt;Neither error named the rule it was enforcing, and that&amp;rsquo;s the category to watch for. When sound logic fails against an unhelpful error, suspect a structural rule about where a thing may live or whether it&amp;rsquo;s even eligible&amp;hellip; not a bug in what you actually wrote. It&amp;rsquo;ll save you an afternoon. It certainly cost me a couple.&lt;/p&gt;</description></item><item><title>Reviewed, then applied</title><link>https://blog-570662.gitlab.io/reviewed-then-applied/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/reviewed-then-applied/</guid><description>&lt;img src="https://blog-570662.gitlab.io/reviewed-then-applied/cover-reviewed-then-applied.png" alt="Featured image of post Reviewed, then applied" /&gt;&lt;p&gt;The genuinely dangerous moment in infrastructure-as-code isn&amp;rsquo;t the apply. It&amp;rsquo;s the gap between the plan a human read and approved, and the change that actually runs a moment later. If those two are different computations (and by default they are) then nobody really reviewed the thing that touched your account. The &lt;code&gt;infra&lt;/code&gt; repo closes that gap from both ends.&lt;/p&gt;
&lt;h2 id="the-gap-between-reviewed-and-ran"&gt;The gap between &amp;ldquo;reviewed&amp;rdquo; and &amp;ldquo;ran&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the moment in infrastructure-as-code where things go wrong.&lt;/p&gt;
&lt;p&gt;Someone opens a merge request. CI runs &lt;code&gt;tofu plan&lt;/code&gt; and the output is there to review: these three resources change, this one is destroyed. A human reads it, decides it&amp;rsquo;s correct, approves, merges. Then &lt;code&gt;apply&lt;/code&gt; runs.&lt;/p&gt;
&lt;p&gt;The trap is in what &lt;code&gt;apply&lt;/code&gt; actually applies. If &lt;code&gt;apply&lt;/code&gt; does its own fresh &lt;code&gt;tofu plan&lt;/code&gt; and then applies &lt;em&gt;that&lt;/em&gt;, the change that runs is not necessarily the change that was reviewed. State can have moved. A provider can have drifted. Someone else can have applied something in between. The reviewed plan and the applied change are two separate computations done at two different moments, and every difference between those moments is a change nobody looked at.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; closes that gap from both ends.&lt;/p&gt;
&lt;h2 id="plan-as-an-artifact"&gt;Plan as an artifact
&lt;/h2&gt;&lt;p&gt;The first end is making the reviewed plan and the applied plan the &lt;em&gt;same object&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;tofu-plan&lt;/code&gt; component runs the plan and saves it. It writes &lt;code&gt;tfplan.cache&lt;/code&gt;, OpenTofu&amp;rsquo;s binary plan file, as a CI artifact. It also writes &lt;code&gt;tfplan.json&lt;/code&gt;, which GitLab renders as a plan widget right in the merge request: the add, change and destroy summary, there to review without leaving the MR.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;tofu-apply&lt;/code&gt; component then does &lt;em&gt;not&lt;/em&gt; re-plan. It applies that saved &lt;code&gt;tfplan.cache&lt;/code&gt;. And OpenTofu itself enforces the safety net: applying a stale plan file, one captured against a state that has since moved, is rejected by the tool. So what reaches the account is provably the plan that was reviewed, or it&amp;rsquo;s nothing at all. There&amp;rsquo;s no third option where something unreviewed slips through.&lt;/p&gt;
&lt;h2 id="applying-is-a-human-decision"&gt;Applying is a human decision
&lt;/h2&gt;&lt;p&gt;The second end is &lt;em&gt;when&lt;/em&gt; apply runs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; is trunk-based: it dropped the &lt;code&gt;develop&lt;/code&gt; branch and works on &lt;code&gt;main&lt;/code&gt;. But a naive trunk setup auto-applies every push to &lt;code&gt;main&lt;/code&gt;, which means there&amp;rsquo;s no human gate at all, just whatever the last merge happened to contain.&lt;/p&gt;
&lt;p&gt;So the gate is built explicitly. &lt;code&gt;releaser-pleaser&lt;/code&gt; keeps a release merge request open against &lt;code&gt;main&lt;/code&gt;. Ordinary merges to &lt;code&gt;main&lt;/code&gt; run plans but apply nothing. The apply happens only when a person &lt;em&gt;merges the release MR&lt;/em&gt;. Merging it cuts a release tag, and the tag pipeline is what runs &lt;code&gt;tofu-apply&lt;/code&gt;, against the plan banked by the latest &lt;code&gt;main&lt;/code&gt; pipeline.&lt;/p&gt;
&lt;p&gt;The effect is that the act of applying to the account is the deliberate, visible act of merging the release request. Nothing reaches the account because a commit landed. It reaches the account because a person decided a release should go out and merged it. (Which, after the &lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;accidental &lt;code&gt;v2.0.0&lt;/code&gt;&lt;/a&gt; that kicked off the whole GitLab move, is a discipline I&amp;rsquo;d freshly relearned the value of.)&lt;/p&gt;
&lt;h2 id="the-guard-on-the-gate"&gt;The guard on the gate
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one more piece, because a gate is only as good as its precondition.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;verify-main-plan&lt;/code&gt; job blocks the release MR from being mergeable unless the latest &lt;code&gt;main&lt;/code&gt; pipeline is green. You can&amp;rsquo;t cut a release, and therefore can&amp;rsquo;t apply, on top of a &lt;code&gt;main&lt;/code&gt; whose plan didn&amp;rsquo;t even succeed. The human gate has its own gate: the thing you&amp;rsquo;re about to merge has to be standing on a known-good plan before you&amp;rsquo;re allowed to merge it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;The risk in infrastructure-as-code is the gap between the plan a human reviewed and the change that runs, because a re-plan at apply time is a different computation from the one that was approved.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; closes it twice over. &lt;code&gt;tofu-plan&lt;/code&gt; saves the plan as a &lt;code&gt;tfplan.cache&lt;/code&gt; artifact and renders it as a merge-request widget; &lt;code&gt;tofu-apply&lt;/code&gt; applies that exact artifact, and OpenTofu rejects it outright if the state has moved underneath it. And applying is gated on a human merging a &lt;code&gt;releaser-pleaser&lt;/code&gt; release request, not on a push, with a &lt;code&gt;verify-main-plan&lt;/code&gt; check making sure that request can only be merged on top of a green plan. What gets applied is what was reviewed, when a person decided it should be.&lt;/p&gt;</description></item><item><title>One graph, not micro-stacks</title><link>https://blog-570662.gitlab.io/one-graph-not-micro-stacks/</link><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-graph-not-micro-stacks/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-graph-not-micro-stacks/cover-one-graph-not-micro-stacks.png" alt="Featured image of post One graph, not micro-stacks" /&gt;&lt;p&gt;Once an infrastructure repo has a few concerns in it (account hardening, the security baseline, the signing stack still to come) there&amp;rsquo;s a steady pressure to split them into separate stacks with separate state, and Terragrunt is right there to help you do it. The &lt;code&gt;infra&lt;/code&gt; repo keeps everything in one OpenTofu graph instead. The reason comes down to who enforces your dependency ordering: the engine, or you.&lt;/p&gt;
&lt;h2 id="the-pressure-to-split"&gt;The pressure to split
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;infra&lt;/code&gt; repo&amp;rsquo;s &lt;code&gt;src/&lt;/code&gt; has several concerns in it, and more coming, the signing stack among them. Once a repo reaches that point, there&amp;rsquo;s a steady pressure to split: one stack per concern, each with its own state file.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s an appealing pressure. Separate stacks feel modular. Each &lt;code&gt;apply&lt;/code&gt; touches less, so the blast radius of any one run is smaller. And Terragrunt exists, popular and well-regarded, precisely to orchestrate a fleet of separate stacks. The path is well trodden.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; didn&amp;rsquo;t take it. &lt;code&gt;src/&lt;/code&gt; is a single OpenTofu root stack: each concern is a &lt;code&gt;module&lt;/code&gt; block, in its own &lt;code&gt;main.&amp;lt;concern&amp;gt;.tf&lt;/code&gt; file, all sharing one state and one graph.&lt;/p&gt;
&lt;h2 id="what-one-graph-gives-you"&gt;What one graph gives you
&lt;/h2&gt;&lt;p&gt;The thing a single graph gives you is engine-enforced truth about ordering and data.&lt;/p&gt;
&lt;p&gt;Inside one OpenTofu graph, the tool builds the full dependency DAG itself. When the signing stack needs a value the security baseline produced, you reference it directly, &lt;code&gt;module.baseline.something&lt;/code&gt;, and OpenTofu &lt;em&gt;guarantees&lt;/em&gt; two things: the baseline is created before the thing that depends on it, and the value handed across is the current one from this same apply. Ordering and data-passing aren&amp;rsquo;t things you arranged. They&amp;rsquo;re facts the engine checks and enforces, every plan, every apply.&lt;/p&gt;
&lt;h2 id="what-splitting-costs"&gt;What splitting costs
&lt;/h2&gt;&lt;p&gt;Split &lt;code&gt;src/&lt;/code&gt; into per-concern stacks with separate state, and that guarantee is the thing you spend.&lt;/p&gt;
&lt;p&gt;Now one stack reads another&amp;rsquo;s outputs through &lt;code&gt;terraform_remote_state&lt;/code&gt;. That&amp;rsquo;s a lookup of a &lt;em&gt;snapshot&lt;/em&gt;: the other stack&amp;rsquo;s last applied state, whatever it was, whenever that was. It&amp;rsquo;s not a live edge in a graph. Ordering is no longer enforced by the engine either; it becomes something you arrange yourself, in CI stage sequencing or in Terragrunt&amp;rsquo;s own dependency blocks.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the trade, stated plainly. You give up a strong, engine-checked guarantee, and you buy back a weaker, hand-arranged imitation of it. Terragrunt is a good tool for managing that weaker world tidily. But the question worth asking first is whether you should be in the weaker world at all.&lt;/p&gt;
&lt;h2 id="when-splitting-is-genuinely-right"&gt;When splitting is genuinely right
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t an argument that splitting is always wrong. Separate states genuinely earn their place when concerns have different change cadences, different access boundaries, or different teams owning them: when you actively &lt;em&gt;want&lt;/em&gt; an apply of one to be unable to touch another, and you want different people holding different state.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; has none of those. It&amp;rsquo;s a single account, a single operator, one cohesive set of concerns. The only thing splitting would buy here is a smaller per-apply blast radius, and that&amp;rsquo;s better handled by reviewing the plan before it applies, which &lt;a class="link" href="https://blog-570662.gitlab.io/reviewed-then-applied/" &gt;the next post&lt;/a&gt; is about, than by fragmenting the dependency graph. So &lt;code&gt;src/&lt;/code&gt; stays one graph, and Terragrunt was considered and deliberately not adopted.&lt;/p&gt;
&lt;h2 id="if-ordering-between-graphs-is-ever-needed"&gt;If ordering between graphs is ever needed
&lt;/h2&gt;&lt;p&gt;If &lt;code&gt;infra&lt;/code&gt; ever does genuinely need more than one stack, the plan isn&amp;rsquo;t Terragrunt. It&amp;rsquo;s to keep each stack a single strong graph internally, and to sequence the stacks with CI stages. Keep the engine-enforced guarantee where it&amp;rsquo;s strongest, inside each graph, and reach for hand-arranged ordering only at the one seam where it&amp;rsquo;s unavoidable.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;A multi-concern infrastructure repo feels like it should be split into per-concern stacks, and Terragrunt is right there to manage the result. &lt;code&gt;infra&lt;/code&gt; keeps &lt;code&gt;src/&lt;/code&gt; as one OpenTofu graph instead.&lt;/p&gt;
&lt;p&gt;Inside one graph, OpenTofu enforces dependency ordering and passes current values across module boundaries as checked facts. Split into separate states and that becomes a &lt;code&gt;terraform_remote_state&lt;/code&gt; snapshot lookup plus ordering you arrange by hand: a weaker version of what you gave up. Splitting is right when concerns have different cadences, boundaries or owners; for a single-account, single-operator repo none of that applies, so the strong guarantee is worth keeping, and Terragrunt is the tool for a problem &lt;code&gt;infra&lt;/code&gt; chose not to have.&lt;/p&gt;</description></item><item><title>CI you include, not copy</title><link>https://blog-570662.gitlab.io/ci-you-include-not-copy/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/ci-you-include-not-copy/</guid><description>&lt;img src="https://blog-570662.gitlab.io/ci-you-include-not-copy/cover-ci-you-include-not-copy.png" alt="Featured image of post CI you include, not copy" /&gt;&lt;p&gt;Every infrastructure repo runs the same CI: lint the OpenTofu, scan it, validate it, plan, apply. The first repo, you write that &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; by hand. The second, you copy it. By the third, you&amp;rsquo;ve got three copies of the same pipeline quietly drifting apart, which is the exact problem you&amp;rsquo;d never tolerate in application code. The &lt;code&gt;cicd&lt;/code&gt; repo is the fix, and it&amp;rsquo;s just the library-first instinct pointed at the pipeline.&lt;/p&gt;
&lt;h2 id="the-gitlab-ciyml-you-keep-copying"&gt;The &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; you keep copying
&lt;/h2&gt;&lt;p&gt;The infrastructure repos in this series all run the same CI gate jobs: format and validate the OpenTofu, lint it, scan it for security issues and secrets, and on the deploy side, plan and apply.&lt;/p&gt;
&lt;p&gt;The first repo, you write that &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; by hand. The second repo needs the same jobs, so you copy it. The third repo, you copy it again. Now there are three copies of the same pipeline, and they do what copies always do. They drift. A fix you make in one repo&amp;rsquo;s CI doesn&amp;rsquo;t reach the other two. A tightened scan rule lands in the repo you were working in and nowhere else. It&amp;rsquo;s the copy-paste problem, exactly as it shows up in application code, just written in YAML and therefore that bit easier to pretend isn&amp;rsquo;t code.&lt;/p&gt;
&lt;h2 id="gitlab-has-a-feature-for-exactly-this"&gt;GitLab has a feature for exactly this
&lt;/h2&gt;&lt;p&gt;GitLab CI/CD Components are the answer to that problem. A component is a reusable, versioned piece of pipeline that you publish, and other projects pull in with an &lt;code&gt;include:&lt;/code&gt; pinned to a version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;gitlab.com/phpboyscout/cicd/tofu-lint@v0.5.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s a library import, for pipeline. The component has a defined interface, a version, and a home in GitLab&amp;rsquo;s CI/CD Catalog. A consuming repo includes it instead of carrying its own copy, and when the component improves, the consumer moves a version pin rather than re-copying YAML.&lt;/p&gt;
&lt;h2 id="why-a-monorepo-of-components"&gt;Why a monorepo of components
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;cicd&lt;/code&gt; repo holds all of the components together: &lt;code&gt;tofu-lint&lt;/code&gt;, &lt;code&gt;tofu-security&lt;/code&gt;, &lt;code&gt;tofu-validate&lt;/code&gt;, &lt;code&gt;tofu-plan&lt;/code&gt;, &lt;code&gt;tofu-apply&lt;/code&gt;, and more. One project, not one project per component.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a deliberate call, and the reason is how GitLab versions things. A version is a tag, and a tag belongs to a &lt;em&gt;project&lt;/em&gt;. A component&amp;rsquo;s version is its project&amp;rsquo;s tag. So a monorepo of components, versioned together as one tag stream, is the natural unit: a consumer pins &lt;code&gt;@v0.5.0&lt;/code&gt; and gets a known-good &lt;em&gt;set&lt;/em&gt; of components that were tested together, rather than juggling a separate version for each one.&lt;/p&gt;
&lt;h2 id="authoring-discipline"&gt;Authoring discipline
&lt;/h2&gt;&lt;p&gt;A component is a file under &lt;code&gt;templates/&lt;/code&gt;, and it opens with a &lt;code&gt;spec: inputs:&lt;/code&gt; block: the typed inputs, their defaults, the component&amp;rsquo;s public interface.&lt;/p&gt;
&lt;p&gt;The discipline that keeps the library usable is that a component must be consumer-agnostic. It never hardcodes a token, and it never names a particular consumer&amp;rsquo;s variable. Inputs have sensible defaults, and a consuming repo overrides them. A component that reaches out and assumes something about the repo including it is a component that works in one repo and surprises the next. An authoring guide in the repo keeps that consistent across everyone who adds a component.&lt;/p&gt;
&lt;h2 id="the-self-test-you-cannot-fully-write"&gt;The self-test you cannot fully write
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;cicd&lt;/code&gt; repo tests its own components with a self-test pipeline. It&amp;rsquo;s worth knowing where that self-test stops.&lt;/p&gt;
&lt;p&gt;When a repo tests its own components by running them in child pipelines, GitLab masks &lt;code&gt;$CI_PIPELINE_SOURCE&lt;/code&gt; as &lt;code&gt;parent_pipeline&lt;/code&gt;. A component&amp;rsquo;s &lt;code&gt;rules:&lt;/code&gt;, which often branch on the pipeline source to behave differently for a merge request than for a branch or a tag, therefore can&amp;rsquo;t be exercised honestly by the self-test: the source they&amp;rsquo;d branch on has been flattened. The self-test covers what it can, and the component &lt;code&gt;rules:&lt;/code&gt; are, in the end, validated by real consumers using them for real. That&amp;rsquo;s a genuine limit, and naming it is better than pretending the self-test proves more than it does. (It&amp;rsquo;s also, not coincidentally, the exact &lt;code&gt;rules:&lt;/code&gt; quirk that bit me in &lt;a class="link" href="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/" &gt;one of the two bugs&lt;/a&gt; I closed the series with.)&lt;/p&gt;
&lt;h2 id="the-same-instinct-again"&gt;The same instinct, again
&lt;/h2&gt;&lt;p&gt;This blog keeps circling the same instinct. go-tool-base exists because the same CLI scaffolding kept getting rewritten, so it was &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;extracted into a library&lt;/a&gt;. &lt;code&gt;cicd&lt;/code&gt; is that instinct pointed at the pipeline: the same gate jobs kept getting copied between repos, so they were extracted into a versioned, included library.&lt;/p&gt;
&lt;p&gt;Stop copy-pasting. Publish, version, include. It&amp;rsquo;s true for CLI code, and it turns out to be just as true for the YAML that builds and ships it.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;Every infrastructure repo needs the same CI, and copying the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; between them produces copies that drift apart. GitLab CI/CD Components fix it: reusable, versioned pipeline that a repo &lt;code&gt;include:&lt;/code&gt;s and pins, instead of carrying its own copy.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cicd&lt;/code&gt; is a monorepo of those components, versioned together as one tag stream, because GitLab tags a project and a component&amp;rsquo;s version is its project&amp;rsquo;s tag. Components are authored consumer-agnostic, with typed &lt;code&gt;spec: inputs:&lt;/code&gt; and no hardcoded assumptions, and their &lt;code&gt;rules:&lt;/code&gt; are validated by real use because the self-test can&amp;rsquo;t see the pipeline source. It&amp;rsquo;s the library-first instinct, applied to CI: publish it once, include it everywhere, fix it in one place.&lt;/p&gt;</description></item><item><title>One image for the whole toolchain</title><link>https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/cover-one-image-for-the-whole-toolchain.png" alt="Featured image of post One image for the whole toolchain" /&gt;&lt;p&gt;Every CI gate job across the infrastructure repos reaches for the same pile of tools: OpenTofu, tflint, trivy, checkov, gitleaks, terraform-docs, the AWS CLI. Installing that pile per job is both slow and quietly dangerous, because nothing pins it consistently. &lt;code&gt;infra-tools&lt;/code&gt; is the obvious fix (one image, one source of truth for versions), but two of its build decisions are less obvious and worth a look: it publishes with &lt;code&gt;crane&lt;/code&gt; instead of a second build, and it deliberately lets its own vulnerability scan fail.&lt;/p&gt;
&lt;h2 id="the-same-pile-of-tools-in-every-repo"&gt;The same pile of tools, in every repo
&lt;/h2&gt;&lt;p&gt;Every infrastructure repo in this series runs the same CI gate jobs: format and validate the OpenTofu, lint it, scan it for security problems and secrets, check the docs. Those jobs need a specific set of tools, and it&amp;rsquo;s the same set in every repo.&lt;/p&gt;
&lt;p&gt;Install them per job and you pay twice. You pay in time, because every pipeline downloads and installs the whole set again. And you pay in drift, because unless every repo pins every tool identically, the repos slowly diverge on which version of trivy or tflint they actually run, and a check that passes in one repo fails in another for no reason anyone can see.&lt;/p&gt;
&lt;h2 id="one-image-one-source-of-truth"&gt;One image, one source of truth
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;infra-tools&lt;/code&gt; is the answer: a single Debian-based container image with the whole toolchain baked in. Every CI job in every repo uses it with one &lt;code&gt;image:&lt;/code&gt; line.&lt;/p&gt;
&lt;p&gt;The real value isn&amp;rsquo;t the convenience. It&amp;rsquo;s that the image is the &lt;em&gt;one place&lt;/em&gt; tool versions are pinned. The Go-based tools are pinned in a &lt;code&gt;mise.toml&lt;/code&gt;. &lt;code&gt;checkov&lt;/code&gt;, which has no mise plugin, is pinned in a requirements file installed with pipx. The AWS CLI is pinned by a build argument. Three mechanisms, because the tools come from three kinds of source, but one image, and every pin wired to Renovate so a version bump arrives as a reviewable pull request. There&amp;rsquo;s exactly one answer to &amp;ldquo;what version of trivy does the toolchain use&amp;rdquo;, and it lives here.&lt;/p&gt;
&lt;h2 id="publishing-with-crane-not-a-second-build"&gt;Publishing with crane, not a second build
&lt;/h2&gt;&lt;p&gt;A build-pipeline detail that took a real bug to discover.&lt;/p&gt;
&lt;p&gt;The pipeline builds the image with kaniko, which builds images without a privileged Docker daemon, something that matters a great deal on shared CI runners. Then it scans the image, then it publishes it.&lt;/p&gt;
&lt;p&gt;The obvious way to write the publish stage is &amp;ldquo;build the image and push it&amp;rdquo;. But kaniko has no mode for &amp;ldquo;just push this tarball I already built&amp;rdquo;. A second kaniko invocation re-executes the entire Dockerfile from the top, including a second &lt;code&gt;mise install&lt;/code&gt;, which makes a fresh round of calls to GitHub&amp;rsquo;s API to fetch tools. GitHub&amp;rsquo;s anonymous API limit is low and shared by IP, so on a CI runner that second install reliably trips a &lt;code&gt;403&lt;/code&gt; rate-limit. (Yes, another &lt;code&gt;403&lt;/code&gt;. They do get everywhere.)&lt;/p&gt;
&lt;p&gt;So the publish stage doesn&amp;rsquo;t rebuild. It uses &lt;code&gt;crane&lt;/code&gt; to push the exact image tarball the build stage already produced. The image is built once. And because the published bytes are the same bytes the scan stage scanned, there&amp;rsquo;s no gap between &amp;ldquo;the image we checked&amp;rdquo; and &amp;ldquo;the image we shipped&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="soft-failing-the-scanner-on-purpose"&gt;Soft-failing the scanner on purpose
&lt;/h2&gt;&lt;p&gt;The decision that looks wrong until you see the reasoning: the pipeline scans the image with trivy, and trivy is allowed to fail without failing the pipeline.&lt;/p&gt;
&lt;p&gt;A vulnerability scanner that doesn&amp;rsquo;t gate the build sounds like a scanner switched off. It isn&amp;rsquo;t. It&amp;rsquo;s a scanner pointed at something it can&amp;rsquo;t helpfully gate.&lt;/p&gt;
&lt;p&gt;The tools in the image are prebuilt Go binaries. trivy inspects them, reads the version of the Go runtime each was compiled with, and reports every known CVE in that Go runtime. Those findings are real, but they aren&amp;rsquo;t &lt;em&gt;mine&lt;/em&gt; to fix. The only fix is the upstream tool rebuilding itself against a patched Go. With seven such tools in the image, at any given moment one of them is usually a little behind on its Go version.&lt;/p&gt;
&lt;p&gt;A hard gate would mean the image becomes unpublishable whenever any single upstream lags, over a CVE in code I don&amp;rsquo;t own and can&amp;rsquo;t patch. That&amp;rsquo;s not a security control; it&amp;rsquo;s a way to be unable to ship. So the scan is &lt;code&gt;allow_failure&lt;/code&gt;. The findings stay fully visible, and the residual count is genuinely useful as a &lt;em&gt;metric&lt;/em&gt; for how far behind upstream the toolchain has drifted. It just doesn&amp;rsquo;t block shipping an image whose only &amp;ldquo;vulnerabilities&amp;rdquo; are other people&amp;rsquo;s build timelines.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;The infrastructure repos all run the same CI gate jobs, needing the same tools, so &lt;code&gt;infra-tools&lt;/code&gt; bakes the whole toolchain into one image and pins every version in one place, wired to Renovate.&lt;/p&gt;
&lt;p&gt;Two build choices are worth copying. The publish stage uses &lt;code&gt;crane&lt;/code&gt; to push the already-built, already-scanned tarball, because a second kaniko build would re-run &lt;code&gt;mise install&lt;/code&gt; and hit GitHub&amp;rsquo;s anonymous rate limit, and because pushing the scanned bytes means shipping exactly what was checked. And the trivy scan is deliberately &lt;code&gt;allow_failure&lt;/code&gt;, because it reports Go-runtime CVEs in prebuilt upstream binaries that no change to this repo can fix, so a hard gate would only make the image unshippable over someone else&amp;rsquo;s lag.&lt;/p&gt;</description></item><item><title>A 403 you can't fix in IAM</title><link>https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/cover-a-403-you-cant-fix-in-iam.png" alt="Featured image of post A 403 you can't fix in IAM" /&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/no-access-keys-in-ci/" &gt;The OIDC post&lt;/a&gt; explained the handshake that lets a GitLab pipeline deploy to AWS with no stored key. This is the story of the first time I got it wrong, and spent an afternoon fixing the wrong thing. The error was a flat 403 from AWS, and the maddening part is that no amount of editing the IAM policy was ever going to fix it.&lt;/p&gt;
&lt;h2 id="a-403-on-the-first-real-run"&gt;A 403 on the first real run
&lt;/h2&gt;&lt;p&gt;The OIDC post covered the handshake: GitLab CI mints a signed token, AWS exchanges it for short-lived credentials against a role whose trust policy names the pipeline. During the GitLab migration I wired exactly that up for the &lt;code&gt;infra&lt;/code&gt; repo, including a trust policy condition meant to let merge-request pipelines run a plan.&lt;/p&gt;
&lt;p&gt;The first merge request that should have triggered &lt;code&gt;tofu-plan&lt;/code&gt; didn&amp;rsquo;t run it. The job failed, and the error from AWS was a flat &lt;code&gt;AccessDenied&lt;/code&gt;. A 403.&lt;/p&gt;
&lt;h2 id="the-instinct-and-why-it-wastes-an-afternoon"&gt;The instinct, and why it wastes an afternoon
&lt;/h2&gt;&lt;p&gt;The instinct on an IAM 403 is immediate and almost always right: the policy&amp;rsquo;s wrong, so go and edit the policy. Tighten the condition. Loosen the condition. Check the wildcard. Re-read the &lt;code&gt;sub&lt;/code&gt; pattern character by character.&lt;/p&gt;
&lt;p&gt;All of that was wasted, and it was wasted for a reason that took me far too long to see. The trust policy wasn&amp;rsquo;t matching the &lt;em&gt;wrong&lt;/em&gt; value. It was matching a value that &lt;em&gt;does not exist&lt;/em&gt;. No amount of editing a condition makes it match a thing that&amp;rsquo;s never present.&lt;/p&gt;
&lt;h2 id="what-is-actually-in-the-token"&gt;What is actually in the token
&lt;/h2&gt;&lt;p&gt;GitLab&amp;rsquo;s OIDC token has a &lt;code&gt;sub&lt;/code&gt; claim that encodes the pipeline&amp;rsquo;s context, and part of that encoding is a &lt;code&gt;ref_type&lt;/code&gt;. I&amp;rsquo;d assumed &lt;code&gt;ref_type&lt;/code&gt; could be &lt;code&gt;branch&lt;/code&gt;, &lt;code&gt;tag&lt;/code&gt;, or &lt;code&gt;mr&lt;/code&gt;, because a pipeline can certainly be a branch pipeline, a tag pipeline, or a merge-request pipeline. So the trust policy, for the plan job, matched a &lt;code&gt;sub&lt;/code&gt; containing &lt;code&gt;ref_type:mr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That assumption was wrong. GitLab&amp;rsquo;s &lt;code&gt;ref_type&lt;/code&gt; is &lt;code&gt;branch&lt;/code&gt; or &lt;code&gt;tag&lt;/code&gt;. That&amp;rsquo;s the entire set. There is no &lt;code&gt;mr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A merge-request pipeline doesn&amp;rsquo;t run against a merge-request ref. It runs against the source &lt;em&gt;branch&lt;/em&gt;. So its token&amp;rsquo;s &lt;code&gt;sub&lt;/code&gt; carries &lt;code&gt;ref_type:branch&lt;/code&gt;, like any other branch pipeline. The trust policy condition asked for &lt;code&gt;ref_type:mr&lt;/code&gt;, GitLab never puts &lt;code&gt;mr&lt;/code&gt; in a token, the condition was therefore never true, and every merge-request pipeline got a 403. Forever, until the policy stopped asking for a claim that isn&amp;rsquo;t real.&lt;/p&gt;
&lt;h2 id="the-fix-and-the-lesson-worth-more-than-the-fix"&gt;The fix, and the lesson worth more than the fix
&lt;/h2&gt;&lt;p&gt;The fix is small once it&amp;rsquo;s visible: match &lt;code&gt;ref_type:branch&lt;/code&gt; and narrow it down by branch name or project path instead. An afternoon of policy edits, and the actual change is one word.&lt;/p&gt;
&lt;p&gt;The lesson is the part worth keeping. When an OIDC trust fails, the useful question is never &amp;ldquo;is my policy clever enough&amp;rdquo;. It&amp;rsquo;s &amp;ldquo;what&amp;rsquo;s &lt;em&gt;actually in the token&lt;/em&gt;&amp;rdquo;. An OIDC trust policy can only ever match the claims the identity provider genuinely asserts, and the gap between what a provider asserts and what you &lt;em&gt;assumed&lt;/em&gt; it asserts is precisely where this class of bug lives.&lt;/p&gt;
&lt;p&gt;So the move, when an OIDC handshake 403s, is to get hold of a real token and decode it. Look at the actual &lt;code&gt;sub&lt;/code&gt;, the actual claims, the actual values. Match what&amp;rsquo;s there. A 403 that survives every sensible edit to the policy is usually not a policy that&amp;rsquo;s too loose or too strict. It&amp;rsquo;s a policy matching a claim that was never going to be in the token.&lt;/p&gt;
&lt;h2 id="the-habit-it-left-behind"&gt;The habit it left behind
&lt;/h2&gt;&lt;p&gt;I wired an OIDC trust policy to let merge-request pipelines plan, by matching a &lt;code&gt;sub&lt;/code&gt; claim with &lt;code&gt;ref_type:mr&lt;/code&gt;. The first real merge request got a 403, and no edit to the policy fixed it, because GitLab&amp;rsquo;s &lt;code&gt;ref_type&lt;/code&gt; is only ever &lt;code&gt;branch&lt;/code&gt; or &lt;code&gt;tag&lt;/code&gt;. A merge-request pipeline runs on a branch ref, so the &lt;code&gt;mr&lt;/code&gt; value the policy demanded was never in any token.&lt;/p&gt;
&lt;p&gt;The fix was one word. The habit it left behind is the valuable bit: when an OIDC trust fails, stop editing the policy and go and read a real token. A trust policy can only match what the provider actually asserts, and &amp;ldquo;what I assumed it asserts&amp;rdquo; is where the 403 was hiding the whole time. (If this shape of bug feels familiar by the end of the series, that&amp;rsquo;s not an accident: I &lt;a class="link" href="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/" &gt;come back to it&lt;/a&gt; with two more from exactly the same family.)&lt;/p&gt;</description></item><item><title>Pure-Rust Git, no git binary</title><link>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</guid><description>&lt;img src="https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/cover-pure-rust-git-no-git-binary.png" alt="Featured image of post Pure-Rust Git, no git binary" /&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves that get confused for one. One half talks to forge APIs (GitHub, GitLab) for releases and pull requests. The other talks to the &lt;code&gt;.git&lt;/code&gt; directory on disk: clone, history, diff, status. This post is mostly about the second half, and specifically about a question that turns out to have three answers in Rust, only one of which I&amp;rsquo;d recommend: how do you actually &lt;em&gt;do&lt;/em&gt; Git from inside a program?&lt;/p&gt;
&lt;h2 id="a-vcs-subsystem-with-two-halves"&gt;A VCS subsystem with two halves
&lt;/h2&gt;&lt;p&gt;go-tool-base has a VCS subsystem, and it does two distinct jobs.&lt;/p&gt;
&lt;p&gt;The first is forge APIs. GitHub and GitLab, Enterprise and nested group paths included. It authenticates, lists releases, fetches release assets, manages pull requests. The self-update machinery sits on this half, and it&amp;rsquo;s what a tool uses to ask &amp;ldquo;what&amp;rsquo;s the latest release?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The second is local Git. go-tool-base also carries a &lt;code&gt;RepoLike&lt;/code&gt; object, an abstraction over an actual Git repository on disk: clone it, read its commit history, diff two trees, check its status. This half doesn&amp;rsquo;t talk to a hosting service at all. It talks to the &lt;code&gt;.git&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;It would be easy to assume the second half grew out of the first. It didn&amp;rsquo;t, and where it actually came from is the part worth telling.&lt;/p&gt;
&lt;h2 id="a-capability-ahead-of-its-consumer"&gt;A capability ahead of its consumer
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;RepoLike&lt;/code&gt; object wasn&amp;rsquo;t built for go-tool-base. It came from another project, where it had already proved itself, and it was pulled into go-tool-base on purpose, with a specific future consumer in mind: the code generator.&lt;/p&gt;
&lt;p&gt;The plan is for the generator to use Git directly. When it scaffolds a new tool, that tool should start life as a Git repository, with a &lt;code&gt;git init&lt;/code&gt; and an initial commit. When you later regenerate, the generator should diff the regenerated template output against your working tree to detect drift, the same idea as &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;respecting your edits&lt;/a&gt;. Both of those are local Git operations, not API calls, so the generator needs a repository abstraction to call into.&lt;/p&gt;
&lt;p&gt;That wiring isn&amp;rsquo;t finished yet. The generator doesn&amp;rsquo;t drive &lt;code&gt;RepoLike&lt;/code&gt; today. But the capability is in place, deliberately, ahead of the consumer that will use it, because the alternative is bolting Git support on later under deadline pressure, and that&amp;rsquo;s how you end up with the wrong abstraction.&lt;/p&gt;
&lt;p&gt;So when rust-tool-base was built, a repository abstraction was never in question. The Rust port carries the same capability for the same reason: a &lt;code&gt;Repo&lt;/code&gt; type with &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;walk&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;blame&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;checkout&lt;/code&gt;, present and ready for the generator to wire into. The open question was never &lt;em&gt;whether&lt;/em&gt; to have it. It was how to &lt;em&gt;do&lt;/em&gt; Git from inside a Rust program, and there are three answers, only one of which is any good.&lt;/p&gt;
&lt;h2 id="three-ways-to-do-git-and-the-one-worth-picking"&gt;Three ways to do Git, and the one worth picking
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Shell out to &lt;code&gt;git&lt;/code&gt;.&lt;/strong&gt; Run the &lt;code&gt;git&lt;/code&gt; binary as a subprocess and parse its output. It works until it doesn&amp;rsquo;t. The binary might not be installed. It might be a different version with different output. Its output is formatted for humans and changes between releases, so parsing it is a standing liability. You&amp;rsquo;ve made an undeclared dependency on a program you don&amp;rsquo;t ship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Link libgit2.&lt;/strong&gt; libgit2 is the C library that reimplements Git as something you can call from code, and &lt;code&gt;git2&lt;/code&gt; is the Rust binding to it. It&amp;rsquo;s solid and widely used. But it&amp;rsquo;s a C dependency, which means a C toolchain in the build, and it&amp;rsquo;s consistently the single biggest source of cross-compilation pain in the Rust Git ecosystem. The musl builds, the Windows builds, the static linking: libgit2 is where they tend to break.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;gix&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;gix&lt;/code&gt; is a reimplementation of Git in pure Rust. No C library, no subprocess. It&amp;rsquo;s just Rust code, and it compiles and cross-compiles like any other crate, because that&amp;rsquo;s all it is. It&amp;rsquo;s also generally faster, and being pure Rust it fits the &lt;a class="link" href="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/" &gt;no-&lt;code&gt;unsafe&lt;/code&gt;-in-first-party-code&lt;/a&gt; story far more comfortably than dragging a C library along.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-vcs&lt;/code&gt; is &lt;code&gt;gix&lt;/code&gt;-first. The &lt;code&gt;Repo&lt;/code&gt; type is built on it. There&amp;rsquo;s no &lt;code&gt;git&lt;/code&gt; binary dependency, and there&amp;rsquo;s no libgit2 in a default build.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gix&lt;/code&gt; is still maturing, and a few write paths, &lt;code&gt;push&lt;/code&gt; in particular, aren&amp;rsquo;t ready in it yet. For those, &lt;code&gt;git2&lt;/code&gt; stays available as an &lt;em&gt;opt-in&lt;/em&gt; fallback behind a Cargo feature. Off by default, so the libgit2 C dependency and its cross-compile cost only land in builds that explicitly ask for push support. The common case, a tool that clones, reads history, diffs and commits, pays none of it. (Which is &lt;a class="link" href="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/" &gt;exactly the feature-flag story&lt;/a&gt; from a couple of weeks back, doing real work.)&lt;/p&gt;
&lt;h2 id="repo-is-a-foundation-not-a-façade"&gt;&lt;code&gt;Repo&lt;/code&gt; is a foundation, not a façade
&lt;/h2&gt;&lt;p&gt;One design decision is worth calling out, because it came straight from a go-tool-base lesson.&lt;/p&gt;
&lt;p&gt;It would have been easy to build &lt;code&gt;Repo&lt;/code&gt; as a narrow façade exposing exactly what the scaffolder and the release-notes feature need today, and nothing else. That was rejected on purpose. go-tool-base&amp;rsquo;s &lt;code&gt;RepoLike&lt;/code&gt; is itself the cautionary tale: it arrived from another project, settled into a sensible abstraction, and is already lined up to carry a consumer, the generator, that wasn&amp;rsquo;t driving its design when it was first written. A repository abstraction gets used by code that doesn&amp;rsquo;t exist yet. Build one as a narrow façade around today&amp;rsquo;s needs and you&amp;rsquo;ve guaranteed a rewrite the first time a downstream tool wants something slightly different.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;rtb-vcs&lt;/code&gt;&amp;rsquo;s &lt;code&gt;Repo&lt;/code&gt; is built as a foundation: a sensible, reasonably complete vocabulary of Git operations that a tool author can compose richer behaviour on, without re-importing &lt;code&gt;gix&lt;/code&gt; directly and re-deriving the framework&amp;rsquo;s auth and concurrency conventions. The errors back this up. &lt;code&gt;gix&lt;/code&gt;&amp;rsquo;s error types aren&amp;rsquo;t leaked through the public API; they&amp;rsquo;re wrapped in semantic &lt;code&gt;RepoError&lt;/code&gt; variants, so the backend could be swapped, &lt;code&gt;gix&lt;/code&gt; to &lt;code&gt;git2&lt;/code&gt;, or to something else entirely, without breaking a single downstream caller.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves: forge-API calls for releases and pull requests, and a &lt;code&gt;RepoLike&lt;/code&gt; object for local Git operations. The repo half arrived from another project and is wired in ahead of its intended consumer, the code generator, which will use it to initialise repositories for scaffolded tools and to diff regenerated output for drift.&lt;/p&gt;
&lt;p&gt;rust-tool-base carries the same capability on purpose. Its &lt;code&gt;Repo&lt;/code&gt; type is built on &lt;code&gt;gix&lt;/code&gt;, a pure-Rust Git implementation, so there&amp;rsquo;s no dependency on an installed &lt;code&gt;git&lt;/code&gt; binary and no libgit2 C library in a default build, which keeps cross-compilation clean. &lt;code&gt;git2&lt;/code&gt; stays an opt-in fallback for the few write paths &lt;code&gt;gix&lt;/code&gt; can&amp;rsquo;t do yet. And &lt;code&gt;Repo&lt;/code&gt; is built as a foundation for downstream tools, with the backend wrapped behind its own error type so it can be replaced without breaking callers.&lt;/p&gt;</description></item><item><title>Routing security findings without the noise</title><link>https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/</guid><description>&lt;img src="https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/cover-routing-security-findings-without-the-noise.png" alt="Featured image of post Routing security findings without the noise" /&gt;&lt;p&gt;Turning on GuardDuty and Security Hub gives you threat detection. It also gives you a firehose. And an alert system that dutifully forwards everything in that firehose isn&amp;rsquo;t monitoring, it&amp;rsquo;s a very efficient way of training your team to ignore alerts. So the &lt;code&gt;alerts&lt;/code&gt; module&amp;rsquo;s real job isn&amp;rsquo;t detection at all. It&amp;rsquo;s deciding what&amp;rsquo;s actually worth interrupting a human for, and the interesting part is everything it deliberately throws away.&lt;/p&gt;
&lt;h2 id="detection-is-the-easy-half"&gt;Detection is the easy half
&lt;/h2&gt;&lt;p&gt;Switching on threat detection in an AWS account is a few resources. GuardDuty, Security Hub with its standards, IAM Access Analyzer: &lt;a class="link" href="https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/" &gt;the security baseline&lt;/a&gt; does exactly that. From then on, the account is generating findings.&lt;/p&gt;
&lt;p&gt;And it generates a lot of them. Plenty are low-severity, informational, or simply the normal texture of a cloud account. If you wire every finding to an email or a pager, you haven&amp;rsquo;t built monitoring. You&amp;rsquo;ve built noise. And noise has a specific failure mode: people stop reading it, and the one finding that genuinely mattered scrolls past unread alongside two hundred that didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;So the valuable work isn&amp;rsquo;t detection. It&amp;rsquo;s &lt;em&gt;routing&lt;/em&gt;: deciding what&amp;rsquo;s worth interrupting a human for, and letting the rest sit quietly in a console for whenever someone reviews it.&lt;/p&gt;
&lt;h2 id="forward-the-severe-leave-the-rest"&gt;Forward the severe, leave the rest
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;alerts&lt;/code&gt; module routes findings with EventBridge rules into an SNS topic that emails out. The rules are deliberately picky. GuardDuty findings are forwarded only at severity 7 and above. Security Hub findings are forwarded only at HIGH and CRITICAL.&lt;/p&gt;
&lt;p&gt;Everything below those thresholds isn&amp;rsquo;t discarded. It&amp;rsquo;s still in GuardDuty and Security Hub, where someone doing a review will see it. It just doesn&amp;rsquo;t get to interrupt anyone&amp;rsquo;s day. The threshold is the line between &amp;ldquo;look at this now&amp;rdquo; and &amp;ldquo;look at this sometime&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="the-duplicate-you-would-otherwise-send-twice"&gt;The duplicate you would otherwise send twice
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the subtle one, and it&amp;rsquo;s the kind of thing you only find by looking closely at where findings come from.&lt;/p&gt;
&lt;p&gt;Security Hub is an aggregator. It pulls findings &lt;em&gt;in&lt;/em&gt; from other services, GuardDuty among them. So a single GuardDuty finding can show up in two places: in GuardDuty itself, and again in Security Hub as an aggregated copy.&lt;/p&gt;
&lt;p&gt;A rule on GuardDuty findings and a rule on Security Hub HIGH/CRITICAL findings would therefore both fire for the same underlying GuardDuty finding. One event, two emails. Do that across an account and a meaningful fraction of your alert volume is just the same findings counted twice, which is its own kind of noise.&lt;/p&gt;
&lt;p&gt;So the Security Hub rule explicitly excludes findings whose &lt;code&gt;ProductName&lt;/code&gt; is GuardDuty, with an &lt;code&gt;anything-but&lt;/code&gt; match. GuardDuty findings come through the GuardDuty rule. The Security Hub rule handles everything Security Hub adds that GuardDuty didn&amp;rsquo;t already report. One finding, one alert, regardless of how many services it passed through.&lt;/p&gt;
&lt;h2 id="two-tripwires-on-the-root-account"&gt;Two tripwires on the root account
&lt;/h2&gt;&lt;p&gt;Findings are about threats the detectors recognise. The module adds two alarms about something simpler: the root account doing anything at all.&lt;/p&gt;
&lt;p&gt;One CloudWatch alarm fires on a root console sign-in. The other fires on any root API call that isn&amp;rsquo;t a console login. In a well-run AWS account, the root user does almost nothing after initial setup: day-to-day work happens through roles. So root activity isn&amp;rsquo;t a &amp;ldquo;finding&amp;rdquo; to be assessed for severity. It&amp;rsquo;s a tripwire. Any of it, in an account that should be silent, is worth an immediate look, and the two alarms say so directly.&lt;/p&gt;
&lt;h2 id="why-a-quiet-alert-stream-matters-here"&gt;Why a quiet alert stream matters here
&lt;/h2&gt;&lt;p&gt;This is monitoring for the account that&amp;rsquo;s going to hold the release-signing key, and that raises the stakes on getting the routing right.&lt;/p&gt;
&lt;p&gt;If a key-bearing account ever does come under attack, the alert that says so has to be &lt;em&gt;seen&lt;/em&gt;. An alert stream that&amp;rsquo;s mostly noise and duplicates is, functionally, no alerting at all, because the people who&amp;rsquo;d act on it have long since tuned it out. Routing the stream down to &amp;ldquo;severe, deduplicated, plus root tripwires&amp;rdquo; is what keeps it something a human will still read on the day it finally matters.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;GuardDuty and Security Hub make detection easy. The hard, valuable part is routing: forwarding what deserves to interrupt someone and leaving the rest in a console.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;alerts&lt;/code&gt; module forwards GuardDuty at severity 7-plus and Security Hub at HIGH/CRITICAL, and it drops the duplicate that aggregation creates by excluding GuardDuty-sourced findings from the Security Hub rule, so one finding is one alert. Two CloudWatch alarms act as tripwires on root-account activity, which should be near-zero. For the account that will hold the signing key, a quiet, trustworthy alert stream isn&amp;rsquo;t a nicety. It&amp;rsquo;s the difference between monitoring and theatre.&lt;/p&gt;</description></item><item><title>Why go-tool-base left GitHub for GitLab</title><link>https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/</guid><description>&lt;img src="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/cover-why-we-left-github-for-gitlab.png" alt="Featured image of post Why go-tool-base left GitHub for GitLab" /&gt;&lt;p&gt;A botched version bump made me stop and actually look at where go-tool-base lived, and I didn&amp;rsquo;t much like what I saw. GitHub had spent months quietly falling over, and when Mitchell Hashimoto (GitHub user #1299, no less) publicly walked Ghostty off the platform, it stopped feeling like just my problem. I&amp;rsquo;ve been a GitLab fan for years, so the move was less a leap and more an overdue nudge. This is the &lt;em&gt;why&lt;/em&gt;, not the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="it-started-with-a-wrong-number"&gt;It started with a wrong number
&lt;/h2&gt;&lt;p&gt;Every migration has a trigger, and mine was embarrassingly small. A commit landed on &lt;code&gt;main&lt;/code&gt; carrying a &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; footer it didn&amp;rsquo;t really deserve. Semantic-release did exactly what it&amp;rsquo;s told to do with that footer: it cut a major version. go-tool-base lurched from the v1 line straight to v2.0.0, and a chain of things that keyed off the version went sideways with it.&lt;/p&gt;
&lt;p&gt;It was fixable. It wasn&amp;rsquo;t a disaster. But it was the kind of small, stupid breakage that makes you stop and actually &lt;em&gt;look&lt;/em&gt; at your setup instead of just patching it and moving on. And when I looked, the version bump wasn&amp;rsquo;t the thing that bothered me. It was everything around it.&lt;/p&gt;
&lt;h2 id="the-platform-had-been-quietly-failing"&gt;The platform had been quietly failing
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d been losing time to GitHub for months. Not dramatically. No single outage you&amp;rsquo;d write home about, just a steady drip of Actions queues that wouldn&amp;rsquo;t drain, pull requests that wouldn&amp;rsquo;t merge, the occasional morning where the thing simply wasn&amp;rsquo;t there. You absorb it. You re-run the job. You make a coffee and try again. You tell yourself it&amp;rsquo;s a blip.&lt;/p&gt;
&lt;p&gt;The trouble with a steady drip is that you stop counting it. It becomes weather.&lt;/p&gt;
&lt;h2 id="the-canary-left-the-mine"&gt;The canary left the mine
&lt;/h2&gt;&lt;p&gt;Then, in late April, &lt;a class="link" href="https://mitchellh.com/" target="_blank" rel="noopener"
 &gt;Mitchell Hashimoto&lt;/a&gt; (co-founder of HashiCorp, creator of Vagrant, Terraform and the Ghostty terminal) published &lt;a class="link" href="https://mitchellh.com/writing/ghostty-leaving-github" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Ghostty Is Leaving GitHub&lt;/em&gt;&lt;/a&gt;, and &lt;em&gt;The Register&lt;/em&gt; &lt;a class="link" href="https://www.theregister.com/software/2026/04/29/mitchell-hashimoto-says-github-no-longer-for-serious-work/5227505" target="_blank" rel="noopener"
 &gt;picked it up&lt;/a&gt; a day later under the headline &amp;ldquo;GitHub &amp;rsquo;no longer a place for serious work&amp;rsquo;&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;This is not a man with a casual relationship to GitHub. He&amp;rsquo;s, by his own account, user #1299, joined February 2008. He called it &amp;ldquo;the place that has made me the most happy&amp;rdquo;. And he still wrote this:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;This is no longer a place for serious work if it just blocks you out for hours per day, every day.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The detail that landed hardest for me wasn&amp;rsquo;t a quote, it was a habit. He&amp;rsquo;d kept a journal for a month, marking an &amp;ldquo;X&amp;rdquo; on every day a GitHub outage had cost him working time. &lt;em&gt;Almost every day had an X.&lt;/em&gt; Reading that, I realised I&amp;rsquo;d been having the same month. I&amp;rsquo;d just never been disciplined enough to write it down. He&amp;rsquo;d turned my vague &amp;ldquo;it&amp;rsquo;s been flaky lately&amp;rdquo; into a row of crosses on a calendar.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I want to ship software and it doesn&amp;rsquo;t want me to ship software.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;When the person who&amp;rsquo;s been on the platform for eighteen years and &lt;em&gt;loves&lt;/em&gt; it says that out loud, it stops being your private grumble. It&amp;rsquo;s the canary, and the canary has stopped singing.&lt;/p&gt;
&lt;h2 id="why-gitlab-and-not-just-somewhere-else"&gt;Why GitLab, and not just &amp;ldquo;somewhere else&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Being annoyed at GitHub is a reason to leave. It is not, on its own, a reason to pick a destination. The destination has to be a positive choice.&lt;/p&gt;
&lt;p&gt;For me GitLab was an easy one, because I&amp;rsquo;ve been a fan for years. Long enough, in fact, to have &lt;em&gt;also&lt;/em&gt; been a reliable grumbler about their pricing tiers, which is how you know it&amp;rsquo;s a real relationship and not a honeymoon. What I&amp;rsquo;ve always rated is the model: GitLab treats source hosting, CI/CD, the package registry, releases and Pages as &lt;em&gt;one integrated product&lt;/em&gt;, not a marketplace of bolted-on parts you assemble yourself.&lt;/p&gt;
&lt;p&gt;That integration is the actual prize. On the old setup, &amp;ldquo;CI&amp;rdquo; meant a folder of separate GitHub Actions workflow files, each pinned, each its own little world. On GitLab it&amp;rsquo;s a single &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline with proper stages (lint, test, security, docs, release) and the release stage talks to the built-in package registry and Pages without me wiring up a single external credential. The CI job that builds the project can authenticate to the things the project needs &lt;em&gt;because they&amp;rsquo;re the same platform&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a second-order benefit too. A migration is a rare licence to fix things you&amp;rsquo;d never otherwise touch. Moving gave me the cover to reset go-tool-base&amp;rsquo;s versioning cleanly (back to a sensible &lt;code&gt;v0.x&lt;/code&gt; line, the accidental &lt;code&gt;v2.0.0&lt;/code&gt; left behind as a cautionary tale) and to move the module path to its new home in one deliberate change rather than a thousand apologetic ones.&lt;/p&gt;
&lt;h2 id="what-im-not-going-to-claim"&gt;What I&amp;rsquo;m not going to claim
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m not going to tell you GitHub is finished, or that GitLab never has a bad day, because it does, everyone does. This isn&amp;rsquo;t a teardown. GitHub gave go-tool-base a perfectly good home for its first year, and the archived mirror is still sitting there, read-only, pointing anyone who finds it at the new place.&lt;/p&gt;
&lt;p&gt;What changed is simpler than a grand verdict. The friction crossed a line, someone I respect said the quiet part loudly enough that I couldn&amp;rsquo;t keep filing it under &amp;ldquo;weather&amp;rdquo;, and the place I&amp;rsquo;d have moved to anyway was sitting right there with a better model. Sometimes the prudent move and the move you secretly wanted turn out to be the same move, and you just need a wrong version number to give you permission.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;go-tool-base moved from GitHub to GitLab in May 2026. The proximate cause was a self-inflicted version-bump mess; the real cause was months of GitHub unreliability that I&amp;rsquo;d stopped consciously noticing until Mitchell Hashimoto&amp;rsquo;s very public departure named it for me. GitLab was a positive pick, not just an escape hatch: its integrated CI/CD, registry, releases and Pages are one product rather than a kit, and that integration is genuinely worth having. The migration also bought a clean versioning restart as a bonus.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been absorbing a steady drip of friction and telling yourself it&amp;rsquo;s normal: try the calendar trick. Mark the X&amp;rsquo;s for a month. The page will tell you something you already half-know.&lt;/p&gt;</description></item><item><title>Why I hand-rolled every module</title><link>https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/</link><pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/</guid><description>&lt;img src="https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/cover-why-i-hand-rolled-every-module.png" alt="Featured image of post Why I hand-rolled every module" /&gt;&lt;p&gt;There are well-known community module libraries for AWS: Cloud Posse, the &lt;code&gt;terraform-aws-modules&lt;/code&gt; collection, plenty more. Both &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; and &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; use almost none of them. Every sub-module is hand-rolled from raw AWS resources, and before you accuse me of not-invented-here syndrome (a perfectly fair first guess), hear me out, because the same evaluation kept landing the same way for a real reason.&lt;/p&gt;
&lt;h2 id="the-promise-of-a-wrapper-module"&gt;The promise of a wrapper module
&lt;/h2&gt;&lt;p&gt;The community module ecosystem makes an appealing offer. Don&amp;rsquo;t write raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; and &lt;code&gt;aws_s3_bucket_policy&lt;/code&gt; and &lt;code&gt;aws_s3_bucket_public_access_block&lt;/code&gt; and the rest. Call a tested, popular module, pass it a handful of inputs, and get a correct, well-configured bucket. Less code in your repo, and the code you don&amp;rsquo;t write has been exercised by thousands of other users.&lt;/p&gt;
&lt;p&gt;For a lot of infrastructure that&amp;rsquo;s a genuinely good deal, and I take it often. For the two infrastructure modules in this series, I took it almost never. Every sub-module is built from raw AWS resources. That wasn&amp;rsquo;t a reflex. It was the same evaluation, made over and over, landing the same way.&lt;/p&gt;
&lt;h2 id="what-kept-going-wrong"&gt;What kept going wrong
&lt;/h2&gt;&lt;p&gt;For each place a wrapper module could have fitted, I looked at the wrapper. And the recurring finding was one of two things. Either using the wrapper &lt;em&gt;correctly&lt;/em&gt;, with all the overrides my posture needed, came to more configuration than the raw resources would have. Or the wrapper&amp;rsquo;s abstraction leaked the instant I needed something it hadn&amp;rsquo;t anticipated, and I was now writing code to fight it.&lt;/p&gt;
&lt;h2 id="the-cloudtrail-bucket-concretely"&gt;The CloudTrail bucket, concretely
&lt;/h2&gt;&lt;p&gt;The clearest example is the bucket that holds CloudTrail logs.&lt;/p&gt;
&lt;p&gt;There are popular modules that set up CloudTrail and bundle an S3 bucket for the logs. Convenient. But that bundled bucket isn&amp;rsquo;t the bucket I want. It doesn&amp;rsquo;t carry &lt;code&gt;lifecycle { prevent_destroy = true }&lt;/code&gt;, and its bucket policy is weaker than the one &lt;a class="link" href="https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/" &gt;the state bucket&lt;/a&gt; taught me to want: TLS-only, SSE-KMS-only, wrong-key-denied.&lt;/p&gt;
&lt;p&gt;So to use the wrapper I had two options. Accept a weaker audit-log bucket than the rest of the account, which rather defeats the point of an audit log. Or fight the wrapper: disable its bucket, create my own, wire it back in. Fighting the wrapper is &lt;em&gt;more&lt;/em&gt; work than simply writing the fifty-odd lines of raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; plus policy that give me exactly the posture I&amp;rsquo;d already designed once. The wrapper didn&amp;rsquo;t save code. It added a negotiation.&lt;/p&gt;
&lt;h2 id="a-wrapper-is-a-deal-and-deals-have-terms"&gt;A wrapper is a deal, and deals have terms
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t an argument that community modules are bad. It&amp;rsquo;s an argument about when the deal is good.&lt;/p&gt;
&lt;p&gt;A wrapper module is a good deal while its abstraction holds: while what it assumes you want matches what you want. The moment you need something it didn&amp;rsquo;t anticipate, the deal inverts. Now you&amp;rsquo;re working &lt;em&gt;against&lt;/em&gt; the abstraction, and an abstraction you&amp;rsquo;re fighting costs more than no abstraction at all. (Regular readers will recognise that line from &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the LangChain argument&lt;/a&gt;; it&amp;rsquo;s the same principle in a very different language.)&lt;/p&gt;
&lt;p&gt;Infrastructure that holds signing keys is precisely the case where you need to control the specifics: every encryption setting, every lifecycle rule, every line of every bucket policy. That&amp;rsquo;s a domain where wrapper abstractions leak fast, because the whole job &lt;em&gt;is&lt;/em&gt; the details the wrapper smoothed over.&lt;/p&gt;
&lt;h2 id="the-cost-paid-on-purpose"&gt;The cost, paid on purpose
&lt;/h2&gt;&lt;p&gt;Hand-rolling isn&amp;rsquo;t free. It&amp;rsquo;s more lines of HCL in the repo, up front, than a one-line module call.&lt;/p&gt;
&lt;p&gt;What those lines buy is worth the price &lt;em&gt;for this kind of infrastructure&lt;/em&gt;. There&amp;rsquo;s no transitive module-version churn to track. There&amp;rsquo;s no abstraction between me and the resource when something behaves oddly. And every line is one I can read, and defend, in a security review, because I wrote it and it says exactly what it does. For a foundation that will hold the most sensitive key in the system, &amp;ldquo;readable and mine&amp;rdquo; beats &amp;ldquo;short and someone else&amp;rsquo;s&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a deliberate trade, not a universal rule. For an internal tool on a deadline, reach for the wrapper. For the security-critical base of everything else, the raw resources won every time I checked.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;The community module ecosystem offers less code that more people have tested, and for plenty of infrastructure that&amp;rsquo;s the right call. For &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; and &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; it almost never was, because each wrapper turned out to be more configuration than the raw resources once my posture was accounted for, or it leaked the moment I needed a specific.&lt;/p&gt;
&lt;p&gt;The CloudTrail log bucket is the pattern in miniature: the bundled bucket lacked &lt;code&gt;prevent_destroy&lt;/code&gt; and a strong policy, so using the wrapper meant either a weaker bucket or fighting the module. A wrapper is a good deal while its abstraction holds and a bad one the moment you fight it, and security-critical foundation infrastructure is all specifics. Hand-rolling cost more lines and bought code I can read and defend. For this, that was the trade worth making.&lt;/p&gt;</description></item><item><title>Hardening the account that will hold the keys</title><link>https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/</link><pubDate>Sat, 09 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/</guid><description>&lt;img src="https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/cover-hardening-the-account-that-will-hold-the-keys.png" alt="Featured image of post Hardening the account that will hold the keys" /&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;Bootstrapping the account&lt;/a&gt; got it &lt;em&gt;ready&lt;/em&gt;: somewhere to store state, an identity to deploy as, enough for the next &lt;code&gt;tofu apply&lt;/code&gt; to run. Ready is not the same as safe. An account with no audit trail, nothing watching it, and no considered way for a human to get in is fine for experimenting and absolutely not where you put the most sensitive key in the system. So before the signing key goes anywhere near it, the account gets a security baseline.&lt;/p&gt;
&lt;h2 id="ready-is-not-the-same-as-safe"&gt;Ready is not the same as safe
&lt;/h2&gt;&lt;p&gt;The bootstrap post ended with an account that was &lt;em&gt;ready&lt;/em&gt;: it had somewhere to store state and a CI identity to deploy as. The next &lt;code&gt;tofu apply&lt;/code&gt; could run.&lt;/p&gt;
&lt;p&gt;Ready is not safe. That account still has no audit trail, so nobody could tell you afterwards what happened in it. It has no threat detection, so nothing is watching. Its defaults are AWS&amp;rsquo;s defaults, which are not a security posture. There&amp;rsquo;s no considered way for a human to get in. An account in that condition is fine for experimenting. It&amp;rsquo;s not somewhere you put the most sensitive key in the whole system.&lt;/p&gt;
&lt;p&gt;So before the signing key is anywhere near it, the account gets a security baseline.&lt;/p&gt;
&lt;h2 id="the-baseline-in-one-downstream-stack"&gt;The baseline, in one downstream stack
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; is that baseline, and it&amp;rsquo;s exactly the downstream stack the bootstrap post promised: applied &lt;em&gt;through&lt;/em&gt; the automation role bootstrap created, not bootstrapped specially.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s six sub-modules, each behind an &lt;code&gt;enable_*&lt;/code&gt; toggle: &lt;code&gt;account-hardening&lt;/code&gt; (IAM password policy, account-wide S3 public-access blocking, default EBS encryption), &lt;code&gt;audit-logging&lt;/code&gt; (a multi-region CloudTrail with log-file validation), &lt;code&gt;aws-config&lt;/code&gt;, &lt;code&gt;threat-detection&lt;/code&gt; (GuardDuty, Security Hub, IAM Access Analyzer), &lt;code&gt;alerts&lt;/code&gt;, and &lt;code&gt;operator-role&lt;/code&gt;. Together they turn a bare account into one that records what happens, watches for trouble, and controls who gets in.&lt;/p&gt;
&lt;p&gt;Most of those are the expected baseline. The operator role is the one worth slowing down on, because it&amp;rsquo;s built backwards from how people usually think about an admin role.&lt;/p&gt;
&lt;h2 id="the-operator-role-and-the-inversion"&gt;The operator role, and the inversion
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;InfraAdmin&lt;/code&gt; is the human way into the account: the role a person assumes to do operator work. Two things define it.&lt;/p&gt;
&lt;p&gt;The trust policy decides &lt;em&gt;who&lt;/em&gt; may assume it. It trusts only the account root principal, and it requires multi-factor authentication: the assume call must carry &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt;, and &lt;code&gt;aws:MultiFactorAuthAge&lt;/code&gt; bounds how recently that MFA was performed. No MFA, no role. So far this is a careful but ordinary admin role.&lt;/p&gt;
&lt;p&gt;The inversion is a &lt;em&gt;second&lt;/em&gt;, separate inline policy, and it&amp;rsquo;s almost entirely &lt;code&gt;Deny&lt;/code&gt;. It denies, using &lt;code&gt;NotAction&lt;/code&gt;, anything where &lt;code&gt;aws:RequestedRegion&lt;/code&gt; falls outside an allowed set of regions. The role&amp;rsquo;s &lt;em&gt;power&lt;/em&gt; comes from an admin grant. This inline policy &lt;em&gt;fences&lt;/em&gt; that power.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the part worth holding onto. People picture an admin role as a list of what it can do. This one is better understood by what it &lt;em&gt;cannot&lt;/em&gt;: it cannot act outside its permitted regions, full stop. A fat-fingered command, or a compromised session, cannot quietly spin resources up in some region nobody&amp;rsquo;s watching. The fence is as much the point of the role as the grant is.&lt;/p&gt;
&lt;h2 id="the-carve-out-because-honesty"&gt;The carve-out, because honesty
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a fiddly detail, and it&amp;rsquo;s the kind of thing that makes the region fence real rather than theoretical.&lt;/p&gt;
&lt;p&gt;Some AWS services are global. IAM, CloudFront, Route 53 and friends have no region, and they don&amp;rsquo;t honour &lt;code&gt;aws:RequestedRegion&lt;/code&gt;. A naive region-deny would therefore deny calls to IAM, and you&amp;rsquo;d lock yourself out of the very service you manage access with. (A close cousin of the kind of self-inflicted lockout I&amp;rsquo;ll come back to in a &lt;a class="link" href="https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/" &gt;later post&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;So the Deny carries explicit carve-outs for the global services. It isn&amp;rsquo;t elegant, and it can&amp;rsquo;t be: the global-versus-regional split is just a fact of AWS, and a correct region fence has to account for it. The carve-out list is the honest cost of the control working.&lt;/p&gt;
&lt;h2 id="harden-the-room-then-move-the-keys-in"&gt;Harden the room, then move the keys in
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an order to all of this, and the order is the argument.&lt;/p&gt;
&lt;p&gt;The account that will hold the signing key has to be audited before the key arrives, so that from day one every call against it is in CloudTrail. It has to be watched before the key arrives, so GuardDuty is already looking. It has to be access-controlled before the key arrives, so the only human path in is MFA-gated and region-fenced.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t move something valuable into a room and then think about locks. You build the room, fit the locks, check they work, and &lt;em&gt;then&lt;/em&gt; move the valuable thing in. The security baseline is fitting the locks. The signing key comes later, into a room already built for it.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Bootstrapping an account makes it ready for the next deploy. It does not make it safe to hold anything that matters. &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; is the downstream stack that closes that gap: audit logging, AWS Config, threat detection, account hardening, and an operator role, applied through the CI role bootstrap created.&lt;/p&gt;
&lt;p&gt;The operator role is the piece to study. It&amp;rsquo;s MFA-gated on the way in, and then fenced by a separate, almost-all-&lt;code&gt;Deny&lt;/code&gt; inline policy that confines it to permitted regions, with carve-outs for the global services that have no region. An admin role defined as much by its fence as its grant. Harden the room first; the keys move in afterwards.&lt;/p&gt;</description></item><item><title>No access keys in CI</title><link>https://blog-570662.gitlab.io/no-access-keys-in-ci/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/no-access-keys-in-ci/</guid><description>&lt;img src="https://blog-570662.gitlab.io/no-access-keys-in-ci/cover-no-access-keys-in-ci.png" alt="Featured image of post No access keys in CI" /&gt;&lt;p&gt;A long-lived AWS access key, sitting in a CI system, is just about the single credential I&amp;rsquo;d most like to be rid of. It&amp;rsquo;s powerful, it never expires unless someone remembers to rotate it (nobody remembers to rotate it), and it lives in one of the most attractive targets in the whole supply chain. For infrastructure that&amp;rsquo;s eventually going to hold a release-signing key, it&amp;rsquo;s exactly the wrong place to start. So the &lt;code&gt;phpboyscout&lt;/code&gt; infrastructure has no AWS access key in CI at all. None.&lt;/p&gt;
&lt;h2 id="the-access-key-you-dont-want"&gt;The access key you don&amp;rsquo;t want
&lt;/h2&gt;&lt;p&gt;A CI pipeline that runs &lt;code&gt;tofu apply&lt;/code&gt; against AWS needs AWS credentials. The traditional way to give it some is an IAM user with an access key pair, pasted into the CI system as a masked variable.&lt;/p&gt;
&lt;p&gt;Look at what that key is. It&amp;rsquo;s long-lived: it works until someone remembers to rotate it, and rotating it is a chore, so mostly nobody does. It&amp;rsquo;s powerful: it can apply infrastructure, so it can do nearly anything. And it&amp;rsquo;s sitting in a CI system, which is one of the most attractive targets in your whole supply chain. You&amp;rsquo;ve taken your highest-value credential and stored a permanent copy of it in a place built for running automated jobs.&lt;/p&gt;
&lt;p&gt;For infrastructure that&amp;rsquo;s going to hold a release-signing key, that&amp;rsquo;s precisely the wrong starting point. So the &lt;code&gt;phpboyscout&lt;/code&gt; infrastructure has no AWS access key in CI at all. Not a well-guarded one. None.&lt;/p&gt;
&lt;h2 id="federation-instead-of-a-stored-secret"&gt;Federation instead of a stored secret
&lt;/h2&gt;&lt;p&gt;The replacement is OIDC federation, and the shape of it is worth walking through, because it&amp;rsquo;s genuinely different from &amp;ldquo;a secret, but better&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;A modern CI platform can mint an OIDC token. GitLab does this with an &lt;code&gt;id_tokens:&lt;/code&gt; block: at job time, GitLab issues a short-lived JSON Web Token, signed by GitLab, that asserts a set of facts. This is project X. This is pipeline Y. This is running on ref Z, of this type.&lt;/p&gt;
&lt;p&gt;AWS can consume that. The &lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt; call takes such a token and, if it satisfies an IAM role&amp;rsquo;s trust policy, returns short-lived AWS credentials for that role. The trust policy is where the control lives: it names GitLab as a trusted token issuer, and it constrains the token&amp;rsquo;s &lt;code&gt;sub&lt;/code&gt; claim so that only the specific project, and the specific refs, you intend can assume the role.&lt;/p&gt;
&lt;p&gt;Put it together: the pipeline asks GitLab for a token, hands it to AWS, and gets back credentials that last about an hour and are scoped to one role. Nothing long-lived is stored anywhere. The credential exists only for the job that needs it, and it can&amp;rsquo;t be stolen from a CI variable store, because it was never in one.&lt;/p&gt;
&lt;h2 id="two-halves-of-one-handshake"&gt;Two halves of one handshake
&lt;/h2&gt;&lt;p&gt;That handshake is built by two of the repos in this series, each owning one side.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&lt;/a&gt; builds the AWS half, in its &lt;code&gt;automation-iam&lt;/code&gt; module: it registers GitLab as an OIDC identity provider in the account, and it creates the automation role with the trust policy that decides which pipelines may assume it.&lt;/p&gt;
&lt;p&gt;The CI components build the consuming half: the &lt;code&gt;id_tokens:&lt;/code&gt; block that asks GitLab for the JWT, and then simply letting the AWS provider&amp;rsquo;s own credential chain perform the exchange. The pipeline doesn&amp;rsquo;t call &lt;code&gt;sts&lt;/code&gt; by hand. It presents the token; the SDK does the rest.&lt;/p&gt;
&lt;h2 id="the-gotcha-dont-set-a-profile"&gt;The gotcha: don&amp;rsquo;t set a profile
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one quiet way to break this, and a stack can look completely correct while doing it.&lt;/p&gt;
&lt;p&gt;The AWS SDK finds credentials by walking a chain of sources in order. The web-identity path, the one that uses the OIDC token, is one link in that chain. It triggers off environment variables the CI sets up automatically.&lt;/p&gt;
&lt;p&gt;But if the &lt;code&gt;aws&lt;/code&gt; provider block has a hardcoded &lt;code&gt;profile = &amp;quot;...&amp;quot;&lt;/code&gt;, the SDK takes the &lt;em&gt;profile&lt;/em&gt; link of the chain instead, and never reaches the web-identity link. A &lt;code&gt;profile&lt;/code&gt; line is the sort of thing that ends up in a provider block from someone&amp;rsquo;s local development setup, where it&amp;rsquo;s exactly right. Committed and run in CI, it silently short-circuits the federation. The pipeline either fails to find credentials, or finds the wrong ones.&lt;/p&gt;
&lt;p&gt;The rule is simple once you know it: the provider block that runs in CI must not name a &lt;code&gt;profile&lt;/code&gt;. Leave the chain free to find the web identity. It&amp;rsquo;s the kind of bug that teaches you to be precise about &lt;em&gt;which&lt;/em&gt; link of the credential chain you&amp;rsquo;re actually relying on.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Giving CI an AWS access key means storing your most powerful, longest-lived credential in one of your most exposed systems. OIDC federation removes it entirely. The CI platform mints a short-lived signed token, AWS exchanges it via &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; for hour-long credentials against a role whose trust policy names the exact pipeline, and nothing permanent is stored.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; builds the AWS side, the identity provider and the trust policy; the CI components build the consuming side, the token request. The one trap is a hardcoded &lt;code&gt;profile&lt;/code&gt; in the provider block, which short-circuits the SDK&amp;rsquo;s credential chain before it reaches the web-identity path. Get that right, and a pipeline deploys to AWS as a verifiable, short-lived identity, with no key to steal.&lt;/p&gt;</description></item><item><title>Secrets that scrub themselves from RAM</title><link>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</guid><description>&lt;img src="https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/cover-secrets-that-scrub-themselves-from-ram.png" alt="Featured image of post Secrets that scrub themselves from RAM" /&gt;&lt;p&gt;A while ago I worked out &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI should keep your API key&lt;/a&gt;: env var, OS keychain, or, grudgingly, a literal in the config file. That answers where the secret &lt;em&gt;lives&lt;/em&gt;. It says nothing about what happens to it once it&amp;rsquo;s loaded and sitting in your process memory, which is the half where secrets actually tend to leak. Rust, it turns out, can do something about that half that Go simply can&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-already-settled"&gt;What go-tool-base already settled
&lt;/h2&gt;&lt;p&gt;A while back I wrote about where a CLI should keep your API keys. The answer go-tool-base settled on was three storage modes, in a fixed precedence: an environment variable reference (the recommended default), the OS keychain (opt-in), or a literal value in the config file (legacy, and refused outright when &lt;code&gt;CI=true&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps that design unchanged. Same three modes, same precedence, same refusal of literal secrets in CI. A tool embeds a &lt;code&gt;CredentialRef&lt;/code&gt; in its typed config, and a &lt;code&gt;Resolver&lt;/code&gt; walks env, then keychain, then literal, then a well-known fallback variable, first hit wins. That part is a straight carry-over, because &lt;em&gt;where&lt;/em&gt; to keep the secret was design, and design survives the port.&lt;/p&gt;
&lt;p&gt;But storage is only half the life of a secret. The other half is what happens to it once it&amp;rsquo;s resolved and sitting in your process memory. That&amp;rsquo;s where Rust can do something Go can&amp;rsquo;t, and rust-tool-base takes the opening.&lt;/p&gt;
&lt;h2 id="the-two-ways-a-secret-leaks-after-youve-loaded-it"&gt;The two ways a secret leaks after you&amp;rsquo;ve loaded it
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve resolved the API key. It&amp;rsquo;s a value in memory now. Two very ordinary things can leak it from there, and neither involves your storage being wrong.&lt;/p&gt;
&lt;p&gt;The first is &lt;strong&gt;the log line&lt;/strong&gt;. Somewhere a developer writes a debug print of a config struct, or an error includes the struct that holds the key, or a panic dumps it. The secret is a string like any other string, so it renders like any other string, straight into a log aggregator that a lot of people can read.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;the leftover bytes&lt;/strong&gt;. The key sat in a heap allocation. The variable goes out of scope, the allocation is freed, and on most runtimes &amp;ldquo;freed&amp;rdquo; just means &amp;ldquo;returned to the allocator&amp;rdquo;. The bytes are still there until something else writes over them. A core dump taken in that window contains your key. So does the next allocation that happens to land on that memory and gets logged before it&amp;rsquo;s overwritten.&lt;/p&gt;
&lt;p&gt;A Go string can&amp;rsquo;t really defend against either. Go strings are immutable, so you can&amp;rsquo;t zero one in place; the runtime copies them freely, so you can&amp;rsquo;t even track every copy; and there&amp;rsquo;s no compile-time barrier stopping anyone printing one. You can be disciplined, but discipline is all you&amp;rsquo;ve got.&lt;/p&gt;
&lt;h2 id="secretstring-closes-both"&gt;&lt;code&gt;SecretString&lt;/code&gt; closes both
&lt;/h2&gt;&lt;p&gt;rust-tool-base routes every secret through &lt;code&gt;secrecy::SecretString&lt;/code&gt;, and the crate is explicit that taking a plain &lt;code&gt;&amp;amp;str&lt;/code&gt; or &lt;code&gt;String&lt;/code&gt; for a secret is a &lt;em&gt;type error&lt;/em&gt;, not a style preference.&lt;/p&gt;
&lt;p&gt;For the log line, &lt;code&gt;SecretString&lt;/code&gt; has its own &lt;code&gt;Debug&lt;/code&gt; implementation, and it prints &lt;code&gt;[REDACTED]&lt;/code&gt;. Always. A config struct holding a &lt;code&gt;SecretString&lt;/code&gt; can be debug-printed, put in an error, caught in a panic, and the secret field shows up as &lt;code&gt;[REDACTED]&lt;/code&gt; every single time. You don&amp;rsquo;t have to remember not to log it. The type already won&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;For the leftover bytes, &lt;code&gt;SecretString&lt;/code&gt; zeroes its memory when it&amp;rsquo;s dropped. When the value goes out of scope, before the allocation is handed back, the bytes are overwritten. The window where a freed allocation still holds your key is closed. A core dump taken afterwards finds zeroes.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a third leak &lt;code&gt;SecretString&lt;/code&gt; blocks that&amp;rsquo;s easy to miss. It deliberately doesn&amp;rsquo;t implement &lt;code&gt;Serialize&lt;/code&gt;. You cannot serialise a &lt;code&gt;SecretString&lt;/code&gt;. That sounds like an inconvenience until you see what it prevents: a tool that loads config, changes one setting, and writes the whole struct back would, with an ordinary string, faithfully write the resolved secret to disk in plain text. Because &lt;code&gt;SecretString&lt;/code&gt; can&amp;rsquo;t be serialised, &lt;code&gt;CredentialRef&lt;/code&gt; can&amp;rsquo;t be either, and that accident is structurally impossible. Writing a secret back is a deliberate, separate path, never a side effect of saving config.&lt;/p&gt;
&lt;p&gt;When code genuinely needs the raw value, to drop it into an &lt;code&gt;Authorization&lt;/code&gt; header, it calls &lt;code&gt;expose_secret()&lt;/code&gt;. The name is the point. Getting at the plaintext is one explicit, greppable, reviewable call, and everywhere else the secret stays wrapped.&lt;/p&gt;
&lt;h2 id="discipline-versus-the-type-system"&gt;Discipline versus the type system
&lt;/h2&gt;&lt;p&gt;The honest framing is this. None of these leaks are exotic. Logging a struct, a core dump after a free, re-saving a config file: they&amp;rsquo;re all routine, and they&amp;rsquo;re all how real credentials end up somewhere they shouldn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s storage design is good, and rust-tool-base kept it. But in Go, &lt;em&gt;not leaking the secret once it&amp;rsquo;s in memory&lt;/em&gt; comes down to every developer being careful every time. In Rust, &lt;code&gt;SecretString&lt;/code&gt; makes the type system carry it. The redaction, the zeroing, the un-serialisability aren&amp;rsquo;t things you remember to do. They&amp;rsquo;re things the secret does to itself because of what it is. That&amp;rsquo;s the part Go structurally can&amp;rsquo;t match, and it&amp;rsquo;s why the port didn&amp;rsquo;t just copy the storage modes across, it tightened the handling underneath them.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;go-tool-base settled where a CLI keeps a secret: env var, keychain, or literal, in a fixed precedence. rust-tool-base keeps that design and hardens what happens once the secret is loaded.&lt;/p&gt;
&lt;p&gt;Every secret is a &lt;code&gt;secrecy::SecretString&lt;/code&gt;. It debug-prints as &lt;code&gt;[REDACTED]&lt;/code&gt;, so it can&amp;rsquo;t fall into a log by accident. Its memory is zeroed on drop, so it doesn&amp;rsquo;t survive in freed heap. It isn&amp;rsquo;t serialisable, so it can&amp;rsquo;t be written back to config by a blanket save. Getting the plaintext is one explicit &lt;code&gt;expose_secret()&lt;/code&gt; call. Go can only ask developers to be careful with a secret in memory; Rust lets the type be careful for them.&lt;/p&gt;</description></item><item><title>The chicken-and-egg of remote state</title><link>https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/cover-the-chicken-and-egg-of-remote-state.png" alt="Featured image of post The chicken-and-egg of remote state" /&gt;&lt;p&gt;Here&amp;rsquo;s a puzzle that every infrastructure-as-code setup hits exactly once, right at the very beginning, and then never again. An OpenTofu stack stores its state in a backend. &lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;The bootstrap stack&lt;/a&gt; I wrote about last time has a particular job, and part of that job is to &lt;em&gt;create&lt;/em&gt; the backend that remote state lives in. So where does the bootstrap stack store its own state, on the very first run, before it&amp;rsquo;s built the place state is supposed to go?&lt;/p&gt;
&lt;h2 id="where-does-the-state-of-the-thing-that-makes-the-state-store-live"&gt;Where does the state of the thing that makes the state store live?
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the puzzle, and it&amp;rsquo;s a real ordering deadlock rather than a riddle.&lt;/p&gt;
&lt;p&gt;An OpenTofu stack keeps a state file, and for anything shared that state file lives in a remote backend: on AWS, an S3 bucket. Fine. But the bootstrap stack has a particular job, and part of that job is to &lt;em&gt;create the S3 bucket that remote state lives in&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So walk through the first run. Bootstrap has never been applied. The state bucket doesn&amp;rsquo;t exist, because creating it is what bootstrap is for. Bootstrap needs somewhere to store its own state. The only place that would make sense is the bucket it&amp;rsquo;s about to create, which isn&amp;rsquo;t there yet. The thing that builds the state store can&amp;rsquo;t store its state in the state store.&lt;/p&gt;
&lt;h2 id="run-local-then-migrate"&gt;Run local, then migrate
&lt;/h2&gt;&lt;p&gt;The way out is a two-step that OpenTofu supports directly.&lt;/p&gt;
&lt;p&gt;Bootstrap starts configured with a &lt;em&gt;local&lt;/em&gt; backend: &lt;code&gt;backend &amp;quot;local&amp;quot; {}&lt;/code&gt;. State is just a file on the operator&amp;rsquo;s machine. With that in place, the first &lt;code&gt;tofu apply&lt;/code&gt; runs. It creates the S3 bucket and the KMS key, and records all of it in the local state file.&lt;/p&gt;
&lt;p&gt;Now the bucket exists. So the backend configuration is rewritten to point at it: an &lt;code&gt;s3&lt;/code&gt; backend block naming the new bucket. Then &lt;code&gt;tofu init -migrate-state&lt;/code&gt;. OpenTofu sees the backend has changed, picks up the local state file, and copies it into the S3 bucket. From that point on, bootstrap&amp;rsquo;s own state lives in the bucket that bootstrap created. The egg has laid the chicken.&lt;/p&gt;
&lt;p&gt;The local backend was a scaffold. It existed for exactly one apply, to break the ordering deadlock, and then the state moved off it and it was never used again.&lt;/p&gt;
&lt;h2 id="it-happened-twice"&gt;It happened twice
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;infra&lt;/code&gt; repo actually did this migration twice, and the second time is the proof that the pattern is general rather than a one-off trick.&lt;/p&gt;
&lt;p&gt;The first migration was the one above: local to S3, at the very start. The second came later, during the &lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;move from GitHub to GitLab&lt;/a&gt;. GitLab offers a managed HTTP state backend, and &lt;code&gt;infra&lt;/code&gt; chose to use it. So the backend block was rewritten again, this time from &lt;code&gt;s3&lt;/code&gt; to &lt;code&gt;http&lt;/code&gt;, and &lt;code&gt;tofu init -migrate-state&lt;/code&gt; ran again, copying the state from the S3 bucket to GitLab&amp;rsquo;s backend.&lt;/p&gt;
&lt;p&gt;The same move, twice, against three different backends. That&amp;rsquo;s the useful lesson hiding in the chicken-and-egg story. State is portable. The backend is just &lt;em&gt;where you currently keep it&lt;/em&gt;, not a property of the stack itself, and moving it is a routine, supported operation rather than surgery.&lt;/p&gt;
&lt;h2 id="why-this-is-the-honest-answer-not-a-hack"&gt;Why this is the honest answer, not a hack
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s easy to look at &amp;ldquo;apply once with a local backend, then migrate&amp;rdquo; and feel it&amp;rsquo;s a bit of a smell, a workaround for something that should have been cleaner.&lt;/p&gt;
&lt;p&gt;It isn&amp;rsquo;t. It&amp;rsquo;s the honest answer to a real ordering problem, and the alternatives are worse.&lt;/p&gt;
&lt;p&gt;The obvious alternative is to create the state bucket by hand, in the console, before running bootstrap at all. But then the most important bucket in the account is unmanaged. It exists outside every OpenTofu graph, nobody&amp;rsquo;s code describes it, its encryption and policy and &lt;code&gt;prevent_destroy&lt;/code&gt; are whatever someone clicked that day, and it drifts. The local-then-migrate dance avoids exactly that. The bucket is created &lt;em&gt;by bootstrap&lt;/em&gt;, described in code, and tracked in bootstrap&amp;rsquo;s own state from its very first apply. It&amp;rsquo;s managed from birth.&lt;/p&gt;
&lt;p&gt;The chicken-and-egg isn&amp;rsquo;t a flaw to be embarrassed about. It&amp;rsquo;s just the shape of the problem when a stack has to build its own foundations, and OpenTofu&amp;rsquo;s &lt;code&gt;-migrate-state&lt;/code&gt; is the supported tool for exactly that shape.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;Every OpenTofu stack needs a backend to store state, and the bootstrap stack&amp;rsquo;s job is to &lt;em&gt;create&lt;/em&gt; the backend, so on its first run the bucket it needs doesn&amp;rsquo;t yet exist.&lt;/p&gt;
&lt;p&gt;The resolution is to run bootstrap once with a local backend, let that apply create the bucket and key, then rewrite the backend configuration and &lt;code&gt;tofu init -migrate-state&lt;/code&gt; the state into the bucket bootstrap just made. The &lt;code&gt;infra&lt;/code&gt; repo did it twice, local to S3 and later S3 to GitLab, which shows the real point: state is portable, and the backend is just where you keep it. Doing it this way, rather than hand-creating the bucket, is what keeps that critical bucket managed in code from its very first day.&lt;/p&gt;</description></item><item><title>A state bucket that defends itself</title><link>https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/cover-a-state-bucket-that-defends-itself.png" alt="Featured image of post A state bucket that defends itself" /&gt;&lt;p&gt;OpenTofu&amp;rsquo;s remote state file is, quietly, the most sensitive thing in an infrastructure repo. It&amp;rsquo;s a plain JSON document listing every resource you manage, every ID, and, depending on your providers, the odd secret in clear text. So the S3 bucket that holds it can&amp;rsquo;t just be a bucket. It has to actively defend itself, on three separate fronts.&lt;/p&gt;
&lt;h2 id="the-most-sensitive-file-in-the-repo"&gt;The most sensitive file in the repo
&lt;/h2&gt;&lt;p&gt;OpenTofu, like Terraform, keeps a state file: a JSON document recording every resource the stack manages, its real-world ID, and its attributes. It&amp;rsquo;s how the tool knows what already exists. It&amp;rsquo;s also, quietly, the most sensitive file in the whole repo. It can hold resource identifiers an attacker would value, and depending on the providers in play it can hold secret values in clear text.&lt;/p&gt;
&lt;p&gt;Three bad things can happen to it. It can be deleted, and now the tool has forgotten everything it manages. It can be read by someone who shouldn&amp;rsquo;t. It can be corrupted by two runs writing at once. The bucket that holds remote state has to defend against all three, and &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&amp;rsquo;s &lt;code&gt;state-backend&lt;/code&gt; module is built around doing exactly that.&lt;/p&gt;
&lt;h2 id="the-dynamodb-lock-table-is-gone"&gt;The DynamoDB lock table is gone
&lt;/h2&gt;&lt;p&gt;Start with the corruption problem, because the answer changed recently.&lt;/p&gt;
&lt;p&gt;The long-standing pattern for remote state on AWS was an S3 bucket &lt;em&gt;plus&lt;/em&gt; a DynamoDB table. S3 held the state; the DynamoDB table held a lock, so two &lt;code&gt;apply&lt;/code&gt; runs couldn&amp;rsquo;t write at once. Everyone who&amp;rsquo;s done Terraform on AWS has provisioned that table, probably more times than they&amp;rsquo;d care to count.&lt;/p&gt;
&lt;p&gt;OpenTofu 1.10 made it unnecessary. The S3 backend gained &lt;code&gt;use_lockfile&lt;/code&gt;, which does the locking with a small lock &lt;em&gt;object&lt;/em&gt; in the same bucket, using S3&amp;rsquo;s conditional-write support. No separate table. The state backend is now genuinely one bucket and one key, with the lock living beside the state. It&amp;rsquo;s one fewer resource to create, one fewer thing to pay for, and one fewer moving part to reason about. The module takes the new path, and the DynamoDB table simply isn&amp;rsquo;t there.&lt;/p&gt;
&lt;h2 id="a-bucket-you-cant-delete-by-accident"&gt;A bucket you can&amp;rsquo;t delete by accident
&lt;/h2&gt;&lt;p&gt;Deletion is guarded with &lt;code&gt;lifecycle { prevent_destroy = true }&lt;/code&gt; on the bucket. With that set, OpenTofu refuses to produce a plan that would destroy the bucket. A stray &lt;code&gt;tofu destroy&lt;/code&gt;, a refactor that drops the resource, an accidental rename: all of them fail loudly instead of quietly taking the state bucket with them.&lt;/p&gt;
&lt;p&gt;This is also why the &lt;code&gt;state-backend&lt;/code&gt; module is hand-rolled from raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; resources rather than wrapping a community module like &lt;code&gt;terraform-aws-modules/s3-bucket&lt;/code&gt;. &lt;code&gt;prevent_destroy&lt;/code&gt; has to sit on the actual resource, and a &lt;code&gt;lifecycle&lt;/code&gt; block isn&amp;rsquo;t something you can pass into a wrapper module as an input. Hand-rolling the bucket keeps &lt;code&gt;prevent_destroy&lt;/code&gt; somewhere you can put it and, just as importantly, somewhere the next reader can see it. (There&amp;rsquo;s a whole post coming on &lt;a class="link" href="https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/" &gt;why I hand-rolled every module&lt;/a&gt;; this is one of the reasons in miniature.)&lt;/p&gt;
&lt;h2 id="reject-anything-encrypted-wrong"&gt;Reject anything encrypted wrong
&lt;/h2&gt;&lt;p&gt;Confidentiality is the subtle one, because the obvious control isn&amp;rsquo;t enough.&lt;/p&gt;
&lt;p&gt;The bucket has a default encryption configuration: server-side encryption with the customer-managed KMS key. But default encryption is a &lt;em&gt;default&lt;/em&gt;. A client making a &lt;code&gt;PutObject&lt;/code&gt; call can override it per request, asking for plain &lt;code&gt;AES256&lt;/code&gt; or a different KMS key, and S3 will honour the override.&lt;/p&gt;
&lt;p&gt;So the module doesn&amp;rsquo;t rely on the default. The bucket policy explicitly denies the upload it doesn&amp;rsquo;t want. It denies any request not over TLS. It denies any &lt;code&gt;PutObject&lt;/code&gt; that isn&amp;rsquo;t using SSE-KMS. And it denies any &lt;code&gt;PutObject&lt;/code&gt; that names the &lt;em&gt;wrong&lt;/em&gt; KMS key. The default encryption config says &amp;ldquo;this is what you get if you don&amp;rsquo;t ask&amp;rdquo;; the bucket policy says &amp;ldquo;and you&amp;rsquo;re not allowed to ask for anything else&amp;rdquo;. State can only ever land encrypted, in transit and at rest, under the one key the module controls.&lt;/p&gt;
&lt;p&gt;One small companion setting: &lt;code&gt;bucket_key_enabled&lt;/code&gt;. With per-object SSE-KMS, every object operation is also a KMS API call, which costs money and can throttle. An S3 Bucket Key collapses those into far fewer KMS calls, cutting per-object KMS traffic by well over ninety per cent. It&amp;rsquo;s a one-line setting the module turns on and most people forget exists.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;Remote state is the most sensitive file an infrastructure repo has, and the bucket that holds it has to defend against deletion, disclosure and corruption.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&amp;rsquo;s state backend handles corruption with OpenTofu 1.10&amp;rsquo;s &lt;code&gt;use_lockfile&lt;/code&gt;, dropping the old DynamoDB lock table entirely. It guards deletion with &lt;code&gt;prevent_destroy&lt;/code&gt;, which is also why the bucket is hand-rolled rather than wrapped. And it guards confidentiality with a bucket policy that denies non-TLS traffic and denies any upload not encrypted with the right KMS key, because default encryption is only a default and a client can override it. The state bucket isn&amp;rsquo;t just a place to put state. It&amp;rsquo;s built to refuse every wrong thing that could happen to it.&lt;/p&gt;</description></item><item><title>Supporting a provider, or actually using it</title><link>https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/</guid><description>&lt;img src="https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/cover-supporting-a-provider-or-actually-using-it.png" alt="Featured image of post Supporting a provider, or actually using it" /&gt;&lt;p&gt;If your CLI tool talks to an AI model, you don&amp;rsquo;t want to hard-wire one vendor. So you reach for a single client interface over several providers, which is the right call. The trap is the next step: build that interface on only what every provider has in common, and you quietly throw away the very features that made you want a particular provider in the first place. rust-tool-base&amp;rsquo;s &lt;code&gt;rtb-ai&lt;/code&gt; refuses to make that trade.&lt;/p&gt;
&lt;h2 id="the-pull-toward-one-interface"&gt;The pull toward one interface
&lt;/h2&gt;&lt;p&gt;If your CLI tool talks to an AI model, hard-wiring one vendor is a poor bet. One user has an Anthropic key, another an OpenAI key. Someone&amp;rsquo;s on Gemini. Someone runs Ollama locally because their data can&amp;rsquo;t leave the building. Someone points at an OpenAI-compatible endpoint from a provider you&amp;rsquo;ve never heard of. You don&amp;rsquo;t want a separate code path for each, so you want one &lt;code&gt;AiClient&lt;/code&gt; that all of them slot behind.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt; gets that unification from the &lt;code&gt;genai&lt;/code&gt; crate, which already speaks to Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible endpoints. One interface, five providers, the tool author picks one in config. The Go sibling makes the same bet: go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package also unifies several providers, behind &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;an interface deliberately kept to four methods&lt;/a&gt;. So far this is the obvious design, and if it were the whole design there&amp;rsquo;d be nothing to write about.&lt;/p&gt;
&lt;h2 id="what-unified-quietly-costs-you"&gt;What &amp;ldquo;unified&amp;rdquo; quietly costs you
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the catch in any unified interface. It can only expose what every provider behind it has in common.&lt;/p&gt;
&lt;p&gt;The common subset is plain chat. Messages go in, text comes out, optionally streamed token by token. That&amp;rsquo;s real and it&amp;rsquo;s useful and every provider does it. But the common subset is also the &lt;em&gt;floor&lt;/em&gt;, and the features that make a particular provider worth choosing are almost never on the floor. They&amp;rsquo;re the things only that provider does.&lt;/p&gt;
&lt;p&gt;Anthropic is the sharp example, because it has three features that matter and not one of them is common-subset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt caching.&lt;/strong&gt; You can mark the stable parts of a request, the system prompt and the tool list, as cacheable. The provider keeps them warm, and on the next turn you aren&amp;rsquo;t billed to re-send and re-process text that didn&amp;rsquo;t change. On a long agent loop, where the same large system prompt rides along on every single turn, that&amp;rsquo;s a substantial saving in both cost and latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extended thinking.&lt;/strong&gt; The model works through a hard problem in a visible, budgeted reasoning pass before it commits to an answer, and you can see that reasoning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citations.&lt;/strong&gt; Structured references back to source material in the response.&lt;/p&gt;
&lt;p&gt;A client built strictly on the common subset can&amp;rsquo;t express any of those. It has no field for them, because four of the five providers wouldn&amp;rsquo;t know what to do with the field. So a purely lowest-common-denominator client would &amp;ldquo;support&amp;rdquo; Anthropic and then use it badly, leaving its best features unreachable. Support as a checkbox, not as the point.&lt;/p&gt;
&lt;h2 id="the-escape-hatch"&gt;The escape hatch
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt;&amp;rsquo;s answer is to not choose. It runs two implementations under one interface.&lt;/p&gt;
&lt;p&gt;For OpenAI, Gemini, Ollama and OpenAI-compatible endpoints, calls route through &lt;code&gt;genai&lt;/code&gt;, the unified path. For Anthropic, every method drops to a direct &lt;code&gt;reqwest&lt;/code&gt; implementation straight against the Messages API. Same &lt;code&gt;AiClient&lt;/code&gt; on the surface, a different implementation underneath, selected by which provider the config names.&lt;/p&gt;
&lt;p&gt;And the request type has deliberate room for the difference:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Anthropic-only: enables prompt caching at every stable point.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Ignored on non-Anthropic providers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache_control&lt;/span&gt;: &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Anthropic-only: extended-thinking budget. `None` disables.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Ignored on non-Anthropic providers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThinkingMode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set &lt;code&gt;cache_control&lt;/code&gt; and the Anthropic-direct path inserts cache breakpoints at the three stable points: the system prompt, the tool list, and the first message. Set &lt;code&gt;thinking&lt;/code&gt; and it adds the thinking block, and streaming surfaces a separate &lt;code&gt;ThinkingToken&lt;/code&gt; event so you can show the reasoning apart from the answer. On a non-Anthropic provider, both fields are simply ignored. The interface carries them; only the implementation that understands them acts on them.&lt;/p&gt;
&lt;h2 id="a-hatch-not-a-leak"&gt;A hatch, not a leak
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being precise about why this isn&amp;rsquo;t the thing it superficially resembles, which is a leaky abstraction.&lt;/p&gt;
&lt;p&gt;A leaky abstraction is one where implementation details bleed through that you didn&amp;rsquo;t intend and can&amp;rsquo;t reason about. The abstraction quietly fails to abstract, and you&amp;rsquo;re left guessing which provider you&amp;rsquo;re really talking to.&lt;/p&gt;
&lt;p&gt;This is the opposite of that. The two Anthropic-only fields aren&amp;rsquo;t a leak. They&amp;rsquo;re named, documented as Anthropic-only, inert everywhere else, and right there in the public type for anyone to see. The interface is uniform for the common case and &lt;em&gt;deliberately, visibly&lt;/em&gt; non-uniform at exactly the points where uniformity would have cost you the good features. You opt into provider-specifics by setting a field. You stay fully portable by leaving it at its default. Nothing bleeds; you decide.&lt;/p&gt;
&lt;p&gt;The same design line explains what &lt;em&gt;does&lt;/em&gt; stay in the unified path. Structured output, &lt;code&gt;chat_structured::&amp;lt;T&amp;gt;&lt;/code&gt;, sends a JSON Schema derived from your Rust type with the request and validates the reply against it before handing you a typed &lt;code&gt;T&lt;/code&gt;. That&amp;rsquo;s a portability win that costs nothing across providers, so it belongs in the common interface. The split isn&amp;rsquo;t &amp;ldquo;Anthropic versus the rest&amp;rdquo;. It&amp;rsquo;s &amp;ldquo;features that are free to unify go in the unified path; features that aren&amp;rsquo;t get a designed door&amp;rdquo;. Prompt caching and extended thinking get the door, because flattening them away would be the expensive kind of convenient.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;A CLI tool that integrates AI wants one client over several providers, and a unified interface can only expose what those providers share. The shared floor is plain chat, and the features worth choosing a provider &lt;em&gt;for&lt;/em&gt;, like Anthropic&amp;rsquo;s prompt caching, extended thinking and citations, are never on the floor.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt; keeps both. &lt;code&gt;genai&lt;/code&gt; provides the unified path across five providers; an Anthropic-direct &lt;code&gt;reqwest&lt;/code&gt; path drops below the abstraction for the features &lt;code&gt;genai&lt;/code&gt; can&amp;rsquo;t reach, and &lt;code&gt;ChatRequest&lt;/code&gt; carries the Anthropic-only fields openly, ignored elsewhere. Uniform where uniformity is free, with a designed escape hatch where it isn&amp;rsquo;t. That&amp;rsquo;s the difference between supporting a provider and actually using it.&lt;/p&gt;</description></item><item><title>Errors without an error handler</title><link>https://blog-570662.gitlab.io/errors-without-an-error-handler/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-without-an-error-handler/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-without-an-error-handler/cover-errors-without-an-error-handler.png" alt="Featured image of post Errors without an error handler" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt; I said go-tool-base&amp;rsquo;s error handler was one of the bits that &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; survive the move to Rust, and promised to come back to it. Here&amp;rsquo;s the come-back. The short version is that Rust hands you, for free, the single consistent error exit that go-tool-base had to build a whole component to get.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-built"&gt;What go-tool-base built
&lt;/h2&gt;&lt;p&gt;A while ago I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;wrote about error handling in go-tool-base&lt;/a&gt;. The core of it: an error should carry a &lt;em&gt;hint&lt;/em&gt;, a separate field of human guidance telling the user what to do next, kept apart from the error&amp;rsquo;s identity so code can still match on it.&lt;/p&gt;
&lt;p&gt;The other half of that post was about consistency. Every go-tool-base command returns its errors the idiomatic Cobra way, and they all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. Presentation decided in exactly one place, so no command can render a failure differently from its neighbour.&lt;/p&gt;
&lt;p&gt;That handler is a real object. It exists, it&amp;rsquo;s wired in, it&amp;rsquo;s the thing every error passes through. Building it was a deliberate piece of work, and it was the right call for Go.&lt;/p&gt;
&lt;p&gt;When I rebuilt this in Rust, the handler didn&amp;rsquo;t survive the move. Not because consistency stopped mattering. Because Rust gives you the single exit for free, and an object to enforce it would just be re-implementing something the language already does for you.&lt;/p&gt;
&lt;h2 id="the-shape-of-a-rust-error"&gt;The shape of a Rust error
&lt;/h2&gt;&lt;p&gt;Start with the type. In rust-tool-base every crate defines its own error enum, and every one of them derives two traits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[derive(Debug, thiserror::Error, miette::Diagnostic)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ConfigError&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;config file not found at {path}&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[diagnostic(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; code(rtb::config::not_found),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; help(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;run `mytool init` to create one, or set MYTOOL_CONFIG&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; )]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;: &lt;span class="nc"&gt;PathBuf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;thiserror::Error&lt;/code&gt; makes it a proper error type. &lt;code&gt;miette::Diagnostic&lt;/code&gt; is the interesting one. A &lt;code&gt;Diagnostic&lt;/code&gt; is an error that also carries the things you&amp;rsquo;d want when &lt;em&gt;presenting&lt;/em&gt; it: a stable &lt;code&gt;code&lt;/code&gt;, a severity, a &lt;code&gt;help&lt;/code&gt; string, and optionally source labels pointing at spans of input. The &lt;code&gt;help&lt;/code&gt; line is the same idea as go-tool-base&amp;rsquo;s hint, the recovery step, except here it&amp;rsquo;s an attribute on the variant rather than a field threaded through a wrapper.&lt;/p&gt;
&lt;p&gt;So the guidance lives on the error, structured, from the moment the error is created.&lt;/p&gt;
&lt;h2 id="there-is-no-handler-theres-a-convention"&gt;There is no handler, there&amp;rsquo;s a convention
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where Rust does the work go-tool-base&amp;rsquo;s handler was built to do.&lt;/p&gt;
&lt;p&gt;A rust-tool-base &lt;code&gt;main&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[tokio::main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;miette&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rtb&lt;/span&gt;::&lt;span class="n"&gt;cli&lt;/span&gt;::&lt;span class="n"&gt;Application&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VersionInfo&lt;/span&gt;::&lt;span class="n"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main&lt;/code&gt; returns &lt;code&gt;miette::Result&amp;lt;()&amp;gt;&lt;/code&gt;. Every command&amp;rsquo;s &lt;code&gt;run&lt;/code&gt; returns a &lt;code&gt;Result&lt;/code&gt; too. In between, errors propagate with the &lt;code&gt;?&lt;/code&gt; operator: a function that hits an error returns it upward, immediately, and the caller does the same, all the way to &lt;code&gt;main&lt;/code&gt;. Nobody writes a &amp;ldquo;check this error&amp;rdquo; call. &lt;code&gt;?&lt;/code&gt; is the propagation.&lt;/p&gt;
&lt;p&gt;And when an error reaches &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; returns it, &lt;em&gt;something&lt;/em&gt; has to render it for the user. That something is a report hook. rust-tool-base installs one at startup, and from then on any &lt;code&gt;Diagnostic&lt;/code&gt; that exits &lt;code&gt;main&lt;/code&gt; is rendered through it: the code, the severity, the help text, the source labels, with colour. One renderer, installed once.&lt;/p&gt;
&lt;p&gt;Look at what that adds up to. Every error in the program flows to one place, &lt;code&gt;main&lt;/code&gt;. It&amp;rsquo;s rendered by one thing, the hook. Presentation is decided in exactly one location and no command can deviate from it. That&amp;rsquo;s precisely the property go-tool-base&amp;rsquo;s &lt;code&gt;ErrorHandler&lt;/code&gt; was built to guarantee. The difference is that nobody built it. The single exit is just where &lt;code&gt;?&lt;/code&gt; propagation ends, and the single renderer is one hook. The language&amp;rsquo;s own convention for returning errors from &lt;code&gt;main&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the funnel.&lt;/p&gt;
&lt;h2 id="errors-are-values-all-the-way"&gt;Errors are values, all the way
&lt;/h2&gt;&lt;p&gt;The thing that took me a moment to fully trust is that there&amp;rsquo;s no funnel to maintain, because there&amp;rsquo;s no funnel as an object. go-tool-base&amp;rsquo;s handler is a component: it can drift, it has to be kept in the path, a command could in principle be wired to bypass it. The Rust version cannot be bypassed, because bypassing it would mean a command not returning its error, and an error you don&amp;rsquo;t return is a compile-time warning at best and dead-obvious wrong code at worst.&lt;/p&gt;
&lt;p&gt;So the model is just: errors are values, you return them, &lt;code&gt;?&lt;/code&gt; carries them up, &lt;code&gt;main&lt;/code&gt; hands the last one to the hook. The consistency isn&amp;rsquo;t enforced by a guard. It&amp;rsquo;s the only thing the shape of the language really lets you do.&lt;/p&gt;
&lt;p&gt;go-tool-base reaches a single, consistent error exit by building one and routing everything through it. rust-tool-base reaches the same exit by having errors be ordinary return values and letting them fall out of &lt;code&gt;main&lt;/code&gt;. Same outcome. One of them is a component you own; the other is a convention you inherit.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;go-tool-base funnels every error through one &lt;code&gt;ErrorHandler&lt;/code&gt; so presentation stays consistent. That handler is a deliberately built component, and it&amp;rsquo;s the right design in Go.&lt;/p&gt;
&lt;p&gt;rust-tool-base has no handler. Every crate&amp;rsquo;s error type derives &lt;code&gt;miette::Diagnostic&lt;/code&gt;, carrying its code, severity and help text. Errors propagate with &lt;code&gt;?&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, which returns &lt;code&gt;miette::Result&lt;/code&gt;, and a framework-installed hook renders whatever comes out. The single consistent exit is the end of &lt;code&gt;?&lt;/code&gt; propagation, and the single renderer is one hook. The funnel go-tool-base built by hand is, in Rust, just the language&amp;rsquo;s return-from-&lt;code&gt;main&lt;/code&gt; convention.&lt;/p&gt;</description></item><item><title>The bootstrap that does almost nothing</title><link>https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/cover-the-bootstrap-that-does-almost-nothing.png" alt="Featured image of post The bootstrap that does almost nothing" /&gt;&lt;p&gt;A brand-new AWS account is a slightly nerve-wracking thing. It can do almost anything, it&amp;rsquo;s hardened against almost nothing, and the list of stuff you ought to set up before you trust it with anything real is long. The natural instinct is to write one big &amp;ldquo;set up the account&amp;rdquo; module that does the whole list in a single apply. I want to talk you out of that, because the bootstrap module I&amp;rsquo;m happiest with does almost nothing, on purpose.&lt;/p&gt;
&lt;h2 id="the-first-apply-problem"&gt;The first-apply problem
&lt;/h2&gt;&lt;p&gt;A brand-new AWS account is not ready for anything serious. Before you&amp;rsquo;d responsibly run real infrastructure into it, you want an account baseline: a password policy, account-wide S3 public-access blocking, default EBS encryption, CloudTrail, AWS Config, GuardDuty, alerting, a sensible human operator role. It&amp;rsquo;s a long list, and all of it matters.&lt;/p&gt;
&lt;p&gt;The instinct, faced with that list, is to write one big &amp;ldquo;set up the account&amp;rdquo; module and have it do everything. One &lt;code&gt;tofu apply&lt;/code&gt;, a fully prepared account, done.&lt;/p&gt;
&lt;p&gt;That instinct is worth resisting, and &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; resists it deliberately.&lt;/p&gt;
&lt;h2 id="three-things-and-a-hard-line"&gt;Three things, and a hard line
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; does three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;state-backend&lt;/code&gt;&lt;/strong&gt;, an S3 bucket and a customer-managed KMS key to hold remote Terraform state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;automation-iam&lt;/code&gt;&lt;/strong&gt;, an OIDC identity provider and an IAM role that CI assumes to apply everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nuke-config&lt;/code&gt;&lt;/strong&gt;, which renders an &lt;a class="link" href="https://github.com/ekristen/aws-nuke" target="_blank" rel="noopener"
 &gt;aws-nuke&lt;/a&gt; configuration scoped to the account, for tearing a throwaway account back down.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the whole module. Account hardening, CloudTrail, AWS Config, GuardDuty, the operator role, the alerting: none of it is in here. And it&amp;rsquo;s not absent by accident. The README has a section headed &amp;ldquo;what&amp;rsquo;s deliberately NOT in scope&amp;rdquo; that lists those exclusions out loud. The boundary is written down, because the boundary is the design.&lt;/p&gt;
&lt;h2 id="why-the-line-is-exactly-there"&gt;Why the line is exactly there
&lt;/h2&gt;&lt;p&gt;The reason the line sits where it does is the most useful idea in the module.&lt;/p&gt;
&lt;p&gt;Everything bootstrap excludes belongs in a &lt;em&gt;separate&lt;/em&gt; stack, applied &lt;em&gt;through the automation role bootstrap creates&lt;/em&gt;. Bootstrap&amp;rsquo;s only job is to get the account to the point where the next &lt;code&gt;tofu apply&lt;/code&gt; can run properly: somewhere to store state, and an identity to run as. Once those two things exist, hardening the account isn&amp;rsquo;t a special bootstrapping act. It&amp;rsquo;s just another apply, done the normal way: in CI, reviewed, versioned, deployed through the role.&lt;/p&gt;
&lt;p&gt;So the account baseline doesn&amp;rsquo;t need to be bundled into the bootstrap. It needs to be &lt;em&gt;downstream&lt;/em&gt; of it. Bootstrap builds the on-ramp; it doesn&amp;rsquo;t also have to be the motorway.&lt;/p&gt;
&lt;h2 id="a-narrow-module-stays-re-runnable"&gt;A narrow module stays re-runnable
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a practical payoff to the narrowness, and it&amp;rsquo;s about fear.&lt;/p&gt;
&lt;p&gt;Bootstrap is the one stack that &lt;em&gt;can&amp;rsquo;t&lt;/em&gt; be applied through CI, because it&amp;rsquo;s what creates the CI identity in the first place. It runs locally, by a human, rarely. That&amp;rsquo;s exactly the kind of operation you want to be small, boring, and safe to repeat.&lt;/p&gt;
&lt;p&gt;A bootstrap module that also did account hardening would be a large, stateful thing managing dozens of resources. Re-running it would be a held-breath operation. Keeping it to three concerns keeps it the opposite: a small stack you can read top to bottom, re-run without anxiety, and reason about completely. The narrowness isn&amp;rsquo;t minimalism for its own sake. It&amp;rsquo;s what keeps the one human-applied stack trustworthy.&lt;/p&gt;
&lt;h2 id="the-boundary-is-the-feature"&gt;The boundary is the feature
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s tempting to judge a module by how much it does. A bootstrap module is the case where that&amp;rsquo;s exactly backwards. Its value is in how cleanly it stops.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; does the bare minimum to make an account ready for the next apply, writes down everything it refuses to do, and hands off to a downstream stack for all of it. The next post follows the trickiest of its three jobs: the state backend has a &lt;a class="link" href="https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/" &gt;genuine chicken-and-egg problem&lt;/a&gt;, because it has to store Terraform state in a bucket Terraform hasn&amp;rsquo;t created yet.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A fresh AWS account needs a long list of things before it&amp;rsquo;s safe, and the obvious move is one big module that does the lot. &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; deliberately does only three: a state backend, a CI identity, and an account-scrub config. Everything else is written down as out of scope.&lt;/p&gt;
&lt;p&gt;The boundary is the design. The excluded work belongs in a downstream stack applied through the CI role bootstrap creates, so hardening is just a normal reviewed apply rather than a bootstrapping special case. And keeping the one human-run, locally-applied stack small is what keeps it safe to re-run. A bootstrap module is judged by where it stops.&lt;/p&gt;</description></item><item><title>Two kinds of feature flag</title><link>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/cover-two-kinds-of-feature-flag.png" alt="Featured image of post Two kinds of feature flag" /&gt;&lt;p&gt;go-tool-base has feature flags: switches that decide which built-in commands are live in a given run. rust-tool-base has those too. But it also has a second, completely separate kind of flag, and the difference between them is one of those distinctions that&amp;rsquo;s obvious the moment you see it and dangerously easy to conflate before you do. One decides what a command &lt;em&gt;does&lt;/em&gt;. The other decides whether a chunk of code is &lt;em&gt;in the binary at all&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="a-workspace-of-crates"&gt;A workspace of crates
&lt;/h2&gt;&lt;p&gt;Before the flags, the shape that makes them possible. go-tool-base is one Go module with packages under &lt;code&gt;pkg/&lt;/code&gt;. rust-tool-base is a Cargo &lt;em&gt;workspace&lt;/em&gt; of seventeen crates: &lt;code&gt;rtb-app&lt;/code&gt;, &lt;code&gt;rtb-config&lt;/code&gt;, &lt;code&gt;rtb-cli&lt;/code&gt;, &lt;code&gt;rtb-vcs&lt;/code&gt;, &lt;code&gt;rtb-ai&lt;/code&gt;, &lt;code&gt;rtb-mcp&lt;/code&gt;, &lt;code&gt;rtb-docs&lt;/code&gt;, &lt;code&gt;rtb-telemetry&lt;/code&gt;, and so on, with an umbrella crate called &lt;code&gt;rtb&lt;/code&gt; that re-exports the public surface.&lt;/p&gt;
&lt;p&gt;That isn&amp;rsquo;t tidiness for its own sake. Each subsystem being a separately compilable crate is what gives you a unit you can include or exclude &lt;em&gt;wholesale&lt;/em&gt;. Hold onto that, because it&amp;rsquo;s the hinge for everything below.&lt;/p&gt;
&lt;h2 id="the-flag-go-tool-base-already-has"&gt;The flag go-tool-base already has
&lt;/h2&gt;&lt;p&gt;go-tool-base has feature flags, and I&amp;rsquo;d describe them as runtime flags. A tool built on it can enable or disable built-in commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Disable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AiCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At startup the framework resolves that set and decides which commands are reachable for this run. The &lt;code&gt;init&lt;/code&gt; command might be present in the binary but switched off; the &lt;code&gt;ai&lt;/code&gt; command might be switched on. It&amp;rsquo;s about the &lt;em&gt;user-facing surface&lt;/em&gt;: which commands exist for someone typing &lt;code&gt;--help&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps this idea. A command carries a &lt;code&gt;CommandSpec&lt;/code&gt; with an optional &lt;code&gt;feature&lt;/code&gt; field, and the runtime decides whether a feature-gated command is reachable. Same purpose: shape the surface per invocation.&lt;/p&gt;
&lt;p&gt;If that were the whole story, there&amp;rsquo;d be nothing to write. The reason there&amp;rsquo;s a post is the &lt;em&gt;other&lt;/em&gt; kind of flag, which Rust makes available and Go really doesn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="the-flag-rust-adds"&gt;The flag Rust adds
&lt;/h2&gt;&lt;p&gt;Cargo features are a compile-time mechanism. The &lt;code&gt;rtb&lt;/code&gt; umbrella crate declares them like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rtb-docs?/ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;vcs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each subsystem is an &lt;em&gt;optional&lt;/em&gt; crate dependency, and a feature switches it on. This is a different kind of switch entirely, and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;A runtime flag decides what a command does &lt;em&gt;while the program runs&lt;/em&gt;. The code is in the binary either way; the flag just gates it.&lt;/p&gt;
&lt;p&gt;A Cargo feature decides what&amp;rsquo;s &lt;em&gt;in the binary in the first place&lt;/em&gt;. Build a tool without the &lt;code&gt;vcs&lt;/code&gt; feature and &lt;code&gt;rtb-vcs&lt;/code&gt; is not compiled. Its dependencies are not compiled. &lt;code&gt;gix&lt;/code&gt;, the pure-Rust Git implementation &lt;code&gt;rtb-vcs&lt;/code&gt; pulls in, roughly two and a half megabytes of it, is not compiled and not linked. It isn&amp;rsquo;t switched off in the binary. It was never in the binary. The compiler never even saw it.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s something a runtime flag cannot do, because by the time anything runs, the binary already exists with everything in it.&lt;/p&gt;
&lt;h2 id="two-axes-kept-separate"&gt;Two axes, kept separate
&lt;/h2&gt;&lt;p&gt;So rust-tool-base has two flag systems answering two genuinely different questions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cargo features&lt;/strong&gt; answer: &lt;em&gt;what is this binary made of?&lt;/em&gt; They&amp;rsquo;re decided when you build the tool, in &lt;code&gt;Cargo.toml&lt;/code&gt;. They control compilation, binary size, dependency surface, and compile time. A tool that never touches Git builds without &lt;code&gt;vcs&lt;/code&gt; and is smaller, faster to compile, and has a smaller dependency tree to audit. A tool that wants everything turns on &lt;code&gt;full&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runtime feature flags&lt;/strong&gt; answer: &lt;em&gt;what can the user do in this run?&lt;/em&gt; They&amp;rsquo;re decided as the program starts. They control which commands appear, which paths are reachable.&lt;/p&gt;
&lt;p&gt;These could have been mashed into one mechanism, and it would have been a mistake. The app-context design notes are blunt about it: feature gating doesn&amp;rsquo;t belong on the per-command context object, because a feature-gated command &amp;ldquo;either exists or doesn&amp;rsquo;t&amp;rdquo; rather than changing its behaviour mid-run. Compile-time composition is one decision, made by the person building the tool. Runtime gating is another, made per invocation. Conflating them would mean you couldn&amp;rsquo;t reason cleanly about either.&lt;/p&gt;
&lt;h2 id="the-go-version-of-this-had-to-be-hand-built"&gt;The Go version of this had to be hand-built
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t a thing Go simply lacks. I &lt;a class="link" href="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/" &gt;wrote a whole post&lt;/a&gt; about how go-tool-base keeps its optional keychain dependency out of binaries that don&amp;rsquo;t want it, using a blank import and the linker&amp;rsquo;s dead-code elimination. It works. But it was a piece of deliberate engineering for &lt;em&gt;one&lt;/em&gt; dependency, and getting it right took care.&lt;/p&gt;
&lt;p&gt;Cargo features make that same outcome a first-class, declarative thing, and not for one dependency but for every subsystem the framework has. You don&amp;rsquo;t engineer the exclusion. You name a feature and leave it off. The crate, and its whole subtree, stays out. Rust&amp;rsquo;s build system was designed for exactly this, and rust-tool-base leans on it across the entire workspace rather than hand-rolling it once.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;go-tool-base has runtime feature flags: they decide, per invocation, which built-in commands are reachable. rust-tool-base keeps that, and adds a second kind that Rust makes available.&lt;/p&gt;
&lt;p&gt;Cargo features decide what the binary is &lt;em&gt;compiled from&lt;/em&gt;. Each of the framework&amp;rsquo;s seventeen crates is an optional dependency, and a feature switched off means that crate and its entire dependency subtree are never compiled or linked. A runtime flag gates what code &lt;em&gt;does&lt;/em&gt;; a Cargo feature gates whether code &lt;em&gt;is there at all&lt;/em&gt;. Two axes, two questions, deliberately kept as separate systems.&lt;/p&gt;</description></item><item><title>A framework that contains no unsafe</title><link>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/cover-a-framework-that-contains-no-unsafe.png" alt="Featured image of post A framework that contains no unsafe" /&gt;&lt;p&gt;&amp;ldquo;It&amp;rsquo;s written in Rust&amp;rdquo; gets thrown around as if it were a memory-safety guarantee. It mostly isn&amp;rsquo;t. Rust is memory-safe by &lt;em&gt;default&lt;/em&gt;, which is a wonderful thing, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists precisely so any crate, any module, can step outside that default when it needs to. So &amp;ldquo;written in Rust&amp;rdquo; really means &amp;ldquo;mostly safe, probably&amp;rdquo;. rust-tool-base makes the stronger claim about its own code, and gets the compiler to enforce it.&lt;/p&gt;
&lt;h2 id="safe-by-default-is-not-the-same-as-safe"&gt;Safe by default is not the same as safe
&lt;/h2&gt;&lt;p&gt;People reach for Rust because of memory safety, and the reputation is earned. Write ordinary Rust and the compiler will not let you have a use-after-free, a data race, or a buffer overrun. That&amp;rsquo;s the default, and it&amp;rsquo;s a very good default.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a default, and defaults can be turned off. Rust has an &lt;code&gt;unsafe&lt;/code&gt; keyword precisely so that, when you genuinely need to, you can dereference a raw pointer, call into C, or tell the compiler you&amp;rsquo;ve upheld an invariant it can&amp;rsquo;t check itself. Inside an &lt;code&gt;unsafe&lt;/code&gt; block, the guarantees are yours to maintain, not the compiler&amp;rsquo;s to enforce.&lt;/p&gt;
&lt;p&gt;That keyword has to exist. Some of the most foundational crates in the ecosystem are built on it, carefully. But it means a fact worth being precise about: a project being &amp;ldquo;written in Rust&amp;rdquo; tells you its code is &lt;em&gt;mostly&lt;/em&gt; safe. It does not tell you the project&amp;rsquo;s own code contains &lt;em&gt;no&lt;/em&gt; &lt;code&gt;unsafe&lt;/code&gt;. Those are different claims, and only the second one is a guarantee.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes the second claim about its own code, and has the compiler back it up.&lt;/p&gt;
&lt;h2 id="forbid-not-just-deny"&gt;&lt;code&gt;forbid&lt;/code&gt;, not just &lt;code&gt;deny&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The mechanism is one line at the top of every crate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#![forbid(unsafe_code)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;unsafe_code&lt;/code&gt; is a lint, and Rust lints have levels. The interesting choice is &lt;code&gt;forbid&lt;/code&gt; rather than &lt;code&gt;deny&lt;/code&gt;, because the two are not the same strength.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; makes the lint an error. But it&amp;rsquo;s an error a &lt;em&gt;downstream module can locally override&lt;/em&gt;. Anyone can write &lt;code&gt;#[allow(unsafe_code)]&lt;/code&gt; on a function or a block and the &lt;code&gt;deny&lt;/code&gt; is lifted right there. As a policy, &lt;code&gt;deny&lt;/code&gt; is &amp;ldquo;don&amp;rsquo;t do this unless you really mean to&amp;rdquo;, and &amp;ldquo;unless you really mean to&amp;rdquo; is a door.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;forbid&lt;/code&gt; is the strict one. It makes the lint an error &lt;em&gt;and&lt;/em&gt; it makes that error impossible to override from inside the crate. A module cannot &lt;code&gt;#[allow]&lt;/code&gt; its way back out. Once a crate root says &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, there&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; anywhere in that crate, and no local exception can be carved out. The compiler simply refuses.&lt;/p&gt;
&lt;p&gt;So every rust-tool-base crate that ships in a built tool forbids &lt;code&gt;unsafe&lt;/code&gt; at its root. Not &amp;ldquo;discourages&amp;rdquo;. Cannot contain it.&lt;/p&gt;
&lt;h2 id="the-one-honest-subtlety"&gt;The one honest subtlety
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a wrinkle, and it&amp;rsquo;s worth showing rather than hiding, because it&amp;rsquo;s where the design got specific.&lt;/p&gt;
&lt;p&gt;The workspace sets &lt;code&gt;unsafe_code = &amp;quot;deny&amp;quot;&lt;/code&gt; as the baseline for &lt;em&gt;everything&lt;/em&gt;, including test files. But test code occasionally has a real need for &lt;code&gt;unsafe&lt;/code&gt;. In the 2024 edition, &lt;code&gt;std::env::set_var&lt;/code&gt; became &lt;code&gt;unsafe&lt;/code&gt;, because mutating the process environment isn&amp;rsquo;t thread-safe, and a test that exercises environment-driven configuration has to call it.&lt;/p&gt;
&lt;p&gt;So the split is deliberate. The workspace-wide level is &lt;code&gt;deny&lt;/code&gt;, which a test file can locally &lt;code&gt;#[allow]&lt;/code&gt; when it genuinely needs that one environment call. But every production &lt;code&gt;lib.rs&lt;/code&gt; and &lt;code&gt;main.rs&lt;/code&gt; additionally carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt; cannot be relaxed. Test scaffolding gets a controlled, visible exception for a specific standard-library call. Shipping code gets none. The guarantee that matters, &amp;ldquo;the code in the binary contains no &lt;code&gt;unsafe&lt;/code&gt;&amp;rdquo;, holds, and the place it&amp;rsquo;s slightly loosened is exactly the place that never reaches a user.&lt;/p&gt;
&lt;h2 id="what-the-guarantee-is-actually-worth"&gt;What the guarantee is actually worth
&lt;/h2&gt;&lt;p&gt;Two things, one for users and one for reviewers.&lt;/p&gt;
&lt;p&gt;For users: an entire family of bug is ruled out of first-party code mechanically. Use-after-free, double-free, data races on shared memory, reading off the end of a buffer. These are the classic memory-safety vulnerabilities, and in a crate that forbids &lt;code&gt;unsafe&lt;/code&gt; they cannot originate, because the constructs that produce them cannot be written. That&amp;rsquo;s not careful coding. It&amp;rsquo;s the compiler refusing to build anything else.&lt;/p&gt;
&lt;p&gt;For reviewers: the cost of an &lt;code&gt;unsafe&lt;/code&gt; block is mostly the review burden it carries. Every one is a spot where a human has to check, by hand, that an invariant holds, and has to re-check it whenever nearby code changes. A crate that forbids &lt;code&gt;unsafe&lt;/code&gt; has zero of those. There&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; block to audit, ever, because the compiler guarantees there isn&amp;rsquo;t one.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll be straight about the boundary: this is a promise about rust-tool-base&amp;rsquo;s &lt;em&gt;own&lt;/em&gt; code. Its dependencies are another matter, and some of them do contain &lt;code&gt;unsafe&lt;/code&gt;, correctly. Keeping that side honest is a different job, done by &lt;a class="link" href="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/" &gt;vetting the dependency tree and gating it in CI&lt;/a&gt;. Within first-party code, though, the guarantee is real, and there&amp;rsquo;s no Go equivalent to it. Go has an &lt;code&gt;unsafe&lt;/code&gt; package, but nothing that lets a codebase prove, to the compiler, that it never touches it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Rust is memory-safe by default, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists so that default can be set aside. &amp;ldquo;Written in Rust&amp;rdquo; therefore does not by itself mean a project&amp;rsquo;s own code contains no &lt;code&gt;unsafe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes that the stronger claim. Every crate root carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt;, unlike &lt;code&gt;deny&lt;/code&gt;, cannot be overridden from inside the crate. Test files get a narrow, visible &lt;code&gt;deny&lt;/code&gt;-level exception for the one standard-library call that needs it; shipping code gets none. The payoff is a whole class of memory-safety bug ruled out of first-party code by construction, and not one &lt;code&gt;unsafe&lt;/code&gt; block left for a reviewer to audit.&lt;/p&gt;</description></item><item><title>Reloading config without a restart</title><link>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</guid><description>&lt;img src="https://blog-570662.gitlab.io/reloading-config-without-a-restart/cover-reloading-config-without-a-restart.png" alt="Featured image of post Reloading config without a restart" /&gt;&lt;p&gt;A config file changes. Someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out? For most processes the answer is blunt: it doesn&amp;rsquo;t, until you restart it. For a short-lived CLI that&amp;rsquo;s completely fine. For a long-running service, &amp;ldquo;just restart it&amp;rdquo; is a much bigger ask than it sounds.&lt;/p&gt;
&lt;h2 id="the-default-answer-is-a-restart"&gt;The default answer is a restart
&lt;/h2&gt;&lt;p&gt;Configuration lives in a file. The file changes: someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out?&lt;/p&gt;
&lt;p&gt;Overwhelmingly, the honest answer is that it doesn&amp;rsquo;t. A process reads its config once, at startup, and that snapshot is frozen for the life of the process. Change the file and nothing happens until you restart, at which point a fresh process reads the fresh file.&lt;/p&gt;
&lt;p&gt;For a short-lived CLI invocation that&amp;rsquo;s completely fine. It reads config, does its job, exits, and the next invocation reads whatever the file says then. But the same frameworks are also used to build long-running services, and for a service &amp;ldquo;just restart it&amp;rdquo; is not the small thing it sounds like.&lt;/p&gt;
&lt;h2 id="what-a-restart-actually-costs"&gt;What a restart actually costs
&lt;/h2&gt;&lt;p&gt;Restarting a long-running service means every open connection drops. Any in-flight request is lost, or has to be retried by whoever sent it. Caches that took real time to warm are cold again. There&amp;rsquo;s a window, short but real, where the service simply isn&amp;rsquo;t serving.&lt;/p&gt;
&lt;p&gt;If the thing you changed was a log level, or a feature flag, or a timeout, you&amp;rsquo;ve paid a disruption wildly out of proportion to the change. And the calculation only gets worse as the service gets more important, because the services you least want to bounce on a whim are exactly the ones that matter most.&lt;/p&gt;
&lt;h2 id="hot-reload-re-read-in-place"&gt;Hot-reload: re-read in place
&lt;/h2&gt;&lt;p&gt;Hot-reload is the alternative, and both go-tool-base and rust-tool-base support it.&lt;/p&gt;
&lt;p&gt;The process doesn&amp;rsquo;t read config once and freeze it. It &lt;em&gt;watches&lt;/em&gt; the config file. When the file changes, it re-reads it, re-applies it, and carries on running. No new process, no dropped connections, no cold start. The change lands in the live process.&lt;/p&gt;
&lt;p&gt;The shape is the same in both frameworks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A file watcher notices the config file changed. Underneath, this is the operating system&amp;rsquo;s own file-notification facility, &lt;code&gt;inotify&lt;/code&gt; on Linux and its equivalents elsewhere. rust-tool-base reaches it through the &lt;code&gt;notify&lt;/code&gt; crate; go-tool-base, through the watcher built into Viper.&lt;/li&gt;
&lt;li&gt;A debounce step waits for the writes to settle. Saving a file is often several separate operations, and you don&amp;rsquo;t want to reload three times for one edit.&lt;/li&gt;
&lt;li&gt;The config is re-parsed from disk.&lt;/li&gt;
&lt;li&gt;The new config is swapped in atomically.&lt;/li&gt;
&lt;li&gt;Observers are notified, so the subsystems that care can react.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps four and five are the ones worth slowing down on, because they&amp;rsquo;re where a naive hot-reload quietly goes wrong.&lt;/p&gt;
&lt;h2 id="the-two-details-that-make-it-safe"&gt;The two details that make it safe
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The atomic swap.&lt;/strong&gt; You do not mutate the live config object in place. A reader on another thread, partway through reading it, would see a torn mix of old and new values, and that&amp;rsquo;s a genuinely nasty class of bug. Instead the process builds a &lt;em&gt;new&lt;/em&gt;, complete config value and swaps the pointer to it in a single atomic operation. Any reader sees either the entire old config or the entire new one, never a blend. rust-tool-base does this with &lt;code&gt;arc-swap&lt;/code&gt;; go-tool-base does the equivalent. Reads stay cheap and lock-free, and an update is one pointer swap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The observer notification.&lt;/strong&gt; Re-reading the file isn&amp;rsquo;t the end of the job. Some subsystems have to &lt;em&gt;do something&lt;/em&gt; when config changes: a connection pool resizes, a logger changes level, a rate limiter takes a new ceiling. So a hot-reload system has to let those subsystems subscribe. rust-tool-base hands observers a &lt;code&gt;watch::Receiver&lt;/code&gt;, a channel that always holds the latest value; go-tool-base exposes an &lt;code&gt;Observable&lt;/code&gt; interface. A subsystem subscribes once and reacts every time config changes, for the life of the process.&lt;/p&gt;
&lt;h2 id="where-this-earns-its-keep-a-kubernetes-pod"&gt;Where this earns its keep: a Kubernetes pod
&lt;/h2&gt;&lt;p&gt;Hot-reload is a nicety on a developer&amp;rsquo;s laptop. Inside a Kubernetes pod it becomes genuinely valuable, and the reason is a neat fit between how Kubernetes delivers config and how a file watcher works.&lt;/p&gt;
&lt;p&gt;In Kubernetes you don&amp;rsquo;t usually bake configuration into the container image. It lives in ConfigMap and Secret objects, and the clean way to consume them is to &lt;em&gt;mount them as volumes&lt;/em&gt;. Mount a ConfigMap as a volume and each key becomes a file in the pod&amp;rsquo;s filesystem.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part that connects to everything above. When you update that ConfigMap or Secret, Kubernetes does not restart your pod. The kubelet notices the object changed and rewrites the projected files inside the still-running pod. The files on disk change underneath a process that never stopped.&lt;/p&gt;
&lt;p&gt;That file rewrite is exactly the event a hot-reload watcher exists to catch. So the whole chain becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You &lt;code&gt;kubectl apply&lt;/code&gt; an updated ConfigMap, or rotate a Secret.&lt;/li&gt;
&lt;li&gt;The kubelet updates the projected files inside the pod.&lt;/li&gt;
&lt;li&gt;The framework&amp;rsquo;s file watcher sees the write.&lt;/li&gt;
&lt;li&gt;The config is re-parsed, swapped in atomically, and observers are notified.&lt;/li&gt;
&lt;li&gt;The new configuration is live, and the pod never cycled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;ve changed a running service, in a running pod, with no rollout, nothing terminated and recreated, no dropped traffic. Rotate a database credential, raise a log level to debug an incident in progress, flip a feature flag: all of it live. For a service where a restart is the very thing you&amp;rsquo;re trying hard to avoid, the kind of &lt;a class="link" href="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/" &gt;long-running service&lt;/a&gt; these frameworks are built for, that&amp;rsquo;s the difference between a config change being routine and being an event.&lt;/p&gt;
&lt;h2 id="the-honest-caveats"&gt;The honest caveats
&lt;/h2&gt;&lt;p&gt;Two things, so this doesn&amp;rsquo;t read as magic.&lt;/p&gt;
&lt;p&gt;First, not everything can be hot-reloaded. Some configuration genuinely needs a restart: the port a server binds to, the size of a thread pool, anything wired up exactly once at process start. Hot-reload covers the large category of settings a subsystem can re-read and re-apply; it doesn&amp;rsquo;t abolish restarts. A config system worth its salt is clear about which settings are live and which are not.&lt;/p&gt;
&lt;p&gt;Second, a Kubernetes gotcha that catches people out. The in-place file update happens for ConfigMaps and Secrets mounted as &lt;em&gt;volumes&lt;/em&gt;. Consume the same ConfigMap as &lt;em&gt;environment variables&lt;/em&gt; instead, and those are fixed when the container starts and never update, short of a restart. If you want hot-reload in a pod, mount config and secrets as files, not env vars. And even with volumes the update isn&amp;rsquo;t instant: the kubelet syncs on a period, around a minute by default, so a reload is &amp;ldquo;within a minute or so&amp;rdquo;, not &amp;ldquo;the moment you hit apply&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A config file changes, and the default way to pick it up is to restart the process. For a long-running service that restart costs dropped connections, lost work and a cold start, often for a change as small as a log level.&lt;/p&gt;
&lt;p&gt;go-tool-base and rust-tool-base both support hot-reload instead: a file watcher catches the change, the config is re-parsed and swapped in atomically so no reader sees torn state, and observers are notified so subsystems can react, all in a live process. The setting where it pays off most is a Kubernetes pod, where ConfigMaps and Secrets mounted as volumes are rewritten in place by the kubelet and the watcher catches that write directly. Mount them as volumes rather than env vars, allow for the kubelet&amp;rsquo;s sync delay, accept that some settings still need a restart, and within those limits &amp;ldquo;the config changed&amp;rdquo; stops meaning &amp;ldquo;cycle the pod&amp;rdquo;.&lt;/p&gt;</description></item><item><title>A signing key needs somewhere to live</title><link>https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/cover-a-signing-key-needs-somewhere-to-live.png" alt="Featured image of post A signing key needs somewhere to live" /&gt;&lt;p&gt;I left a door open a couple of posts ago, and it&amp;rsquo;s been quietly bothering me ever since. When I wrote about &lt;a class="link" href="https://blog-570662.gitlab.io/verifying-your-own-downloads/" &gt;verifying your own downloads&lt;/a&gt;, I was honest that a checksum sitting next to the binary only catches accidents. Anyone who can compromise the release platform can swap the binary and the checksum together, and the tool will happily verify one fake against the other.&lt;/p&gt;
&lt;p&gt;Closing that gap needs a signature. And a signature, it turns out, needs a surprising amount of infrastructure standing behind it. This is the first post about building that.&lt;/p&gt;
&lt;h2 id="the-door-the-last-post-left-open"&gt;The door the last post left open
&lt;/h2&gt;&lt;p&gt;A while back I wrote about verifying your own downloads: go-tool-base&amp;rsquo;s self-update command now checks the SHA-256 of every binary it downloads against the release&amp;rsquo;s published &lt;code&gt;checksums.txt&lt;/code&gt; before installing it.&lt;/p&gt;
&lt;p&gt;That post was honest about its own ceiling. A checksum file hosted &lt;em&gt;next to&lt;/em&gt; the binary it describes shares a trust root with that binary. Both come from the same release, on the same platform. Corruption, truncation, a CDN serving a stale object: a same-origin checksum catches all of those, because they&amp;rsquo;re accidents and the checksum wasn&amp;rsquo;t part of the accident. What it can&amp;rsquo;t catch is an attacker who&amp;rsquo;s compromised the release platform itself. Someone who can replace the binary can replace &lt;code&gt;checksums.txt&lt;/code&gt; in the same breath, and the tool will cheerfully verify the malicious download against the malicious checksum and call it good.&lt;/p&gt;
&lt;p&gt;The post named the fix and then deferred it: a signature whose trust root sits somewhere the release platform can&amp;rsquo;t reach. &amp;ldquo;That&amp;rsquo;s the next phase of this work.&amp;rdquo; This series is that phase.&lt;/p&gt;
&lt;h2 id="what-a-signature-actually-needs"&gt;What a signature actually needs
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being precise about why a signature helps where a checksum doesn&amp;rsquo;t, because it&amp;rsquo;s easy to wave the word &amp;ldquo;signature&amp;rdquo; around and assume it settles everything.&lt;/p&gt;
&lt;p&gt;A signature closes the gap only under two conditions. The verifying key, the public half, must reach the user by a path the release platform doesn&amp;rsquo;t control. And the signing key, the private half, must live somewhere the release platform can&amp;rsquo;t reach.&lt;/p&gt;
&lt;p&gt;The second condition is the one people skip. If the signing key sits in the same CI system that builds the release, you&amp;rsquo;ve gained almost nothing. An attacker who owns the CI owns the key, and a key they own will sign whatever they hand it. The signature verifies perfectly and means precisely nothing. A signature is only worth the distance between the signing key and the thing being signed. Put them in the same place and the distance is zero.&lt;/p&gt;
&lt;p&gt;So the signing key has to live in a different security domain from the release pipeline. Not a different folder. A different account, with a different blast radius, that the release platform has no standing access to.&lt;/p&gt;
&lt;h2 id="just-sign-the-binary-is-not-a-small-feature"&gt;&amp;ldquo;Just sign the binary&amp;rdquo; is not a small feature
&lt;/h2&gt;&lt;p&gt;That reframes a line item that sounds tiny. &amp;ldquo;Sign the release binary&amp;rdquo; unpacks into a list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;there must be a private signing key;&lt;/li&gt;
&lt;li&gt;it must live outside the release platform, in its own security domain;&lt;/li&gt;
&lt;li&gt;it must be access-controlled, audited, and protected from exfiltration;&lt;/li&gt;
&lt;li&gt;only the release pipeline may ask it to sign, and only by proving a short-lived, federated identity, never by holding a copy of the key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s not a feature you bolt onto a CLI. That&amp;rsquo;s infrastructure.&lt;/p&gt;
&lt;p&gt;The shape of it: a cloud account, with the key held in a managed key service so the private key material never exists as a file on a disk that anyone, me included, can copy. The release pipeline authenticates to that account as itself, briefly, and asks the key service to produce a signature. The key never moves.&lt;/p&gt;
&lt;p&gt;But an account you&amp;rsquo;re going to trust with a signing key is itself something you have to get right first. An account with a weak baseline, no audit trail, and long-lived credentials lying around is not a safe home for the most security-sensitive key in the whole system. Before the key can move in, the house has to be built and the locks have to actually work.&lt;/p&gt;
&lt;h2 id="what-this-series-builds"&gt;What this series builds
&lt;/h2&gt;&lt;p&gt;So this turned into a rather longer project than &amp;ldquo;add a signature&amp;rdquo;, and the series follows it in order.&lt;/p&gt;
&lt;p&gt;It starts with bootstrapping a fresh AWS account: the deliberately minimal first &lt;code&gt;tofu apply&lt;/code&gt;, and the remote state backend that has a genuine chicken-and-egg problem. Then the credential question, which is the heart of it: how a CI pipeline deploys to AWS with no stored access key at all. Then hardening the account, so it&amp;rsquo;s genuinely safe to hold something valuable. Then the discipline of deploying changes to it: plans reviewed before they&amp;rsquo;re applied. Then the shared tooling that makes all of it repeatable.&lt;/p&gt;
&lt;p&gt;Every one of those pieces exists for the same reason. The signing key needs somewhere to live, and somewhere safe is not a default you&amp;rsquo;re handed. It&amp;rsquo;s a thing you build, deliberately, before you have anything worth protecting in it.&lt;/p&gt;
&lt;p&gt;The series ends where the verifying-downloads post pointed: a signing service whose key the release platform can&amp;rsquo;t touch, so a self-updating tool can finally verify that the binary it&amp;rsquo;s about to become is genuinely the one I published.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s self-update verifies downloads against a checksum, and a same-origin checksum stops accidents but not a compromise of the release platform. The fix is a signature, and a signature is only worth the distance between its signing key and the release pipeline.&lt;/p&gt;
&lt;p&gt;Holding that key safely means a private key that never leaves a managed key service, in a separate cloud account, reached only by a short-lived federated identity. That&amp;rsquo;s infrastructure, and a safe account is something you build before you trust it with anything. The rest of this series builds it, piece by piece, right up to the signing service itself.&lt;/p&gt;</description></item><item><title>Waivers with an expiry date</title><link>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</guid><description>&lt;img src="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/cover-waivers-with-an-expiry-date.png" alt="Featured image of post Waivers with an expiry date" /&gt;&lt;p&gt;A vulnerability scanner gives you a yes or a no. Is there a known advisory on a path you actually use? Yes, or no. That&amp;rsquo;s genuinely useful, and you should run one. But it&amp;rsquo;s a snapshot, taken on the day you ask, and supply-chain risk in a framework is a bigger and more ongoing thing than a single yes-or-no can capture.&lt;/p&gt;
&lt;p&gt;So rust-tool-base treats its whole dependency tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, not something to scan and forget.&lt;/p&gt;
&lt;h2 id="a-scanner-answers-one-question"&gt;A scanner answers one question
&lt;/h2&gt;&lt;p&gt;When I &lt;a class="link" href="https://blog-570662.gitlab.io/every-finding-was-the-same-shape/" &gt;had go-tool-base security-audited&lt;/a&gt;, part of the routine was running a vulnerability scanner over the dependencies. Go has a good one. It looks at your dependency graph, cross-references known advisories, and tells you whether any of them reach code you actually call.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s useful and you should do it. But notice the shape of what it gives back: essentially a yes or a no. Either there&amp;rsquo;s a known vulnerability on a reachable path or there isn&amp;rsquo;t. It answers one question, on the day you ask it.&lt;/p&gt;
&lt;p&gt;Supply-chain risk in a framework is broader than that one question, because a framework drags its entire dependency tree into every tool built on it. rust-tool-base treats the whole tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, and the tool for that is &lt;code&gt;cargo-deny&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-gate-not-a-scan"&gt;A gate, not a scan
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cargo-deny&lt;/code&gt; reads a &lt;code&gt;deny.toml&lt;/code&gt; and checks the dependency graph against four kinds of rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Licences.&lt;/strong&gt; There&amp;rsquo;s an allowlist: MIT, Apache-2.0, the BSD variants, ISC, a handful of others. Every transitive crate&amp;rsquo;s licence has to be on it. A dependency that pulls in something copyleft, or something with no licence at all, fails the build. You find out the first time it enters the tree, not during a release scramble when someone finally reads the legal implications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advisories.&lt;/strong&gt; It checks the RustSec advisory database, and yanked crates are set to &lt;code&gt;deny&lt;/code&gt;, so a dependency that&amp;rsquo;s been pulled from the registry stops CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bans.&lt;/strong&gt; Wildcard version requirements (&lt;code&gt;version = &amp;quot;*&amp;quot;&lt;/code&gt;) are denied outright, because a dependency that floats to whatever&amp;rsquo;s newest is a supply-chain hole by construction. Duplicate versions of the same crate get surfaced too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Crates may only come from the official registry. An unknown registry or a stray git dependency is denied. Nothing sneaks in from a URL.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a gate. It encodes, as rules in a file, what the project will and won&amp;rsquo;t accept into its dependency tree, and it enforces them on every build instead of once an audit.&lt;/p&gt;
&lt;h2 id="the-honest-part-is-the-waiver-list"&gt;The honest part is the waiver list
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing every real project runs into. Sooner or later there&amp;rsquo;s an advisory you genuinely can&amp;rsquo;t fix this week. It&amp;rsquo;s against a crate three levels down your tree. The fix needs an upstream release that hasn&amp;rsquo;t happened. The crate is scheduled to be reworked two milestones from now anyway. The gate is going to fail, and the work to satisfy it honestly isn&amp;rsquo;t available to you yet.&lt;/p&gt;
&lt;p&gt;The lazy response is a blanket ignore: silence the advisory, move on, forget. Now your gate has a hole in it that nobody remembers opening.&lt;/p&gt;
&lt;p&gt;rust-tool-base&amp;rsquo;s &lt;code&gt;deny.toml&lt;/code&gt; does something better. Every waiver in the &lt;code&gt;ignore&lt;/code&gt; list is a documented record. Each one carries a comment that names the crate, traces the &lt;em&gt;exact dependency path&lt;/em&gt; that reaches it, gives the reason, and names the condition that lifts it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `instant` - reached via async-openai -&amp;gt; backoff -&amp;gt; rtb-ai (v0.3).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0384&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `paste` - reached via ratatui -&amp;gt; rtb-docs (v0.2) / rtb-tui (v0.4).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0436&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file states the policy out loud: &amp;ldquo;Every waiver points at a deferred stub crate that will be reworked before its ship milestone. Lift each waiver when the owning crate lands its v0.1.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Some waivers go further and carry a structured reason field, so the &lt;em&gt;why&lt;/em&gt; travels with the entry rather than living only in a comment above it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2025-0140&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gix-date via gix is a stub dependency; rtb-vcs v0.5 will upgrade&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read that list and you don&amp;rsquo;t see a project that quietly stopped caring about seven advisories. You see seven advisories the project knows about, can trace, and has tied to a specific milestone. The waiver has an expiry condition. When &lt;code&gt;rtb-vcs&lt;/code&gt; reaches v0.5, that &lt;code&gt;gix&lt;/code&gt; entry is meant to come out, and the comment is the reminder that it should.&lt;/p&gt;
&lt;h2 id="why-this-is-the-bit-to-copy"&gt;Why this is the bit to copy
&lt;/h2&gt;&lt;p&gt;A gate that can&amp;rsquo;t be relaxed is a gate people route around. They&amp;rsquo;ll find the broadest possible ignore and use it, because the alternative is being blocked on someone else&amp;rsquo;s release. The pressure to do that is real, and it&amp;rsquo;s not unreasonable.&lt;/p&gt;
&lt;p&gt;So the design that actually holds up isn&amp;rsquo;t a stricter gate. It&amp;rsquo;s a gate with an honest, structured escape hatch: you &lt;em&gt;can&lt;/em&gt; waive an advisory, but a waiver costs you a documented record with a dependency path and an expiry condition. That price is small enough that nobody routes around it, and high enough that waivers don&amp;rsquo;t accumulate silently. The &lt;code&gt;ignore&lt;/code&gt; list stays readable, and every line in it is something you could defend out loud.&lt;/p&gt;
&lt;p&gt;Supply-chain hygiene framed this way isn&amp;rsquo;t an audit you survive once a year. It&amp;rsquo;s bookkeeping: a ledger of what you accepted, why, and when each exception is due to close. Which, now I write it down, is just the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;Boy Scout rule&lt;/a&gt; again, pointed at a dependency tree. Leave it tidier than you found it, and write down the bits you couldn&amp;rsquo;t tidy yet.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A vulnerability scanner answers one question on one day. &lt;code&gt;cargo-deny&lt;/code&gt; is a standing policy gate: licences against an allowlist, advisories and yanked crates denied, wildcard versions banned, sources restricted to the official registry, enforced on every build.&lt;/p&gt;
&lt;p&gt;The part of rust-tool-base&amp;rsquo;s setup worth copying is the waiver list. Every advisory that can&amp;rsquo;t be fixed yet is recorded with its crate, its dependency path, its reason and the milestone that removes it. A waiver is a dated note, not a shrug, and that&amp;rsquo;s what keeps the gate honest enough that nobody actually wants to bypass it.&lt;/p&gt;</description></item><item><title>A builder that won't compile if you forget a field</title><link>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/cover-a-builder-that-wont-compile-if-you-forget-a-field.png" alt="Featured image of post A builder that won't compile if you forget a field" /&gt;&lt;p&gt;go-tool-base configures things with functional options, and if you forget a required one, the best case is a runtime failure and the worst case is an empty value sailing silently into everything downstream. Most builder patterns share the same hole. rust-tool-base closes it in a way I find genuinely delightful: the &lt;code&gt;.build()&lt;/code&gt; method simply doesn&amp;rsquo;t &lt;em&gt;exist&lt;/em&gt; until you&amp;rsquo;ve set every required field.&lt;/p&gt;
&lt;h2 id="when-is-a-required-field-actually-required"&gt;When is a required field actually required
&lt;/h2&gt;&lt;p&gt;Every framework has constructors with a mix of required and optional inputs. An &lt;code&gt;Application&lt;/code&gt; in rust-tool-base needs tool metadata and a version. It optionally takes a custom config type, extra commands, feature toggles. The metadata needs a name and a summary; a description and a help channel are optional.&lt;/p&gt;
&lt;p&gt;The interesting question is &lt;em&gt;when&lt;/em&gt; &amp;ldquo;required&amp;rdquo; gets enforced. There are really only two moments available: when the program runs, or when it compiles. Most APIs pick the first without ever framing it as a choice.&lt;/p&gt;
&lt;h2 id="how-go-tool-base-does-it"&gt;How go-tool-base does it
&lt;/h2&gt;&lt;p&gt;go-tool-base uses functional options, the standard Go pattern:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;New&lt;/code&gt; takes a variadic list of options and applies them. It&amp;rsquo;s flexible and it reads well. But look at what the &lt;em&gt;type&lt;/em&gt; actually says. &lt;code&gt;New&lt;/code&gt; accepts zero or more options. The signature is satisfied by passing nothing at all. If &lt;code&gt;WithName&lt;/code&gt; is required, nothing in the type system knows that. Forget it and the code compiles cleanly, and you find out when the program runs, or worse, when it doesn&amp;rsquo;t visibly fail but quietly carries an empty name into everything downstream.&lt;/p&gt;
&lt;p&gt;A plain builder is no better here. &lt;code&gt;builder.name(&amp;quot;mytool&amp;quot;).build()&lt;/code&gt; and &lt;code&gt;builder.build()&lt;/code&gt; are both perfectly valid calls as far as the compiler is concerned. The builder &lt;em&gt;hopes&lt;/em&gt; you set the name. It can check at the end and return an error, but that check still happens at runtime.&lt;/p&gt;
&lt;p&gt;In every one of these the required-ness of a field is a fact that lives in documentation and in the author&amp;rsquo;s head, not in the code.&lt;/p&gt;
&lt;h2 id="typestate-putting-required-in-the-type"&gt;Typestate: putting &amp;ldquo;required&amp;rdquo; in the type
&lt;/h2&gt;&lt;p&gt;rust-tool-base builds these with &lt;code&gt;bon&lt;/code&gt;, and the pattern it generates is a &lt;em&gt;typestate&lt;/em&gt; builder. The idea is that the builder&amp;rsquo;s type changes as you call it, and that type tracks which required fields you&amp;rsquo;ve set so far.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ToolMetadata::builder()&lt;/code&gt; returns a builder in a state that records &amp;ldquo;name not set, summary not set&amp;rdquo;. Calling &lt;code&gt;.name(...)&lt;/code&gt; consumes that builder and returns a &lt;em&gt;different type&lt;/em&gt;, one whose state records &amp;ldquo;name set&amp;rdquo;. Calling &lt;code&gt;.summary(...)&lt;/code&gt; does the same for the summary.&lt;/p&gt;
&lt;p&gt;The part that matters is &lt;code&gt;.build()&lt;/code&gt;. It isn&amp;rsquo;t a method on the builder in general. It only exists on the builder type that represents &amp;ldquo;every required field has been set&amp;rdquo;. So this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;doesn&amp;rsquo;t compile. Not because a runtime check fired, but because in the state &amp;ldquo;name not set&amp;rdquo; there&amp;rsquo;s no &lt;code&gt;.build()&lt;/code&gt; method to call in the first place. The compiler stops you, and the error points straight at the missing &lt;code&gt;.name(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Optional fields stay optional. You can call &lt;code&gt;.description(...)&lt;/code&gt; or skip it, and &lt;code&gt;.build()&lt;/code&gt; is reachable either way, because the description was never part of the state that gates it. The required and the optional are genuinely different in the type, which is exactly the distinction the functional-options version could only keep in a comment.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Application::builder()&lt;/code&gt; works the same way. It won&amp;rsquo;t produce an &lt;code&gt;Application&lt;/code&gt; until it has metadata and a version, and &amp;ldquo;won&amp;rsquo;t&amp;rdquo; there means the method is absent, not that a check returns &lt;code&gt;Err&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="why-the-moment-matters"&gt;Why the moment matters
&lt;/h2&gt;&lt;p&gt;Moving the check from run time to compile time changes who finds the mistake, and when.&lt;/p&gt;
&lt;p&gt;A runtime check finds it when that code path executes, which might be in a test, might be in CI, might be on a user&amp;rsquo;s machine at the worst possible moment. A compile-time check finds it the instant you write it, in the editor, before anything has run at all. The same mistake, caught at the cheapest possible point instead of one of the expensive ones.&lt;/p&gt;
&lt;p&gt;It also changes what the API &lt;em&gt;documents about itself&lt;/em&gt;. A functional-options constructor can&amp;rsquo;t tell you, from its signature alone, which options you must pass. A typestate builder can, because the set of methods available to you at each step &lt;em&gt;is&lt;/em&gt; the documentation. You literally cannot reach &lt;code&gt;.build()&lt;/code&gt; without having been walked past every required field on the way.&lt;/p&gt;
&lt;p&gt;This is one of those places where Rust&amp;rsquo;s type system earns its reputation. The builder isn&amp;rsquo;t more careful than the Go version. It&amp;rsquo;s that &amp;ldquo;this field is required&amp;rdquo; stopped being a convention and became something the compiler enforces. (Another entry, if you&amp;rsquo;re keeping score from &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt;, in the column of outcomes that survived while the Go mechanism got left behind.)&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;Required fields have to be enforced somewhere. Functional options and ordinary builders enforce them at runtime, if at all, because &lt;code&gt;.build()&lt;/code&gt; is always callable and the type system never learns which inputs were mandatory.&lt;/p&gt;
&lt;p&gt;rust-tool-base uses typestate builders generated by &lt;code&gt;bon&lt;/code&gt;. The builder&amp;rsquo;s type changes as you set fields, and &lt;code&gt;.build()&lt;/code&gt; only exists once every required field is present. Forgetting one is a compile error that names the missing call, not a runtime surprise. The required-versus-optional distinction stops being a comment and becomes part of the type.&lt;/p&gt;</description></item><item><title>Registering commands without life before main</title><link>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</guid><description>&lt;img src="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/cover-registering-commands-without-life-before-main.png" alt="Featured image of post Registering commands without life before main" /&gt;&lt;p&gt;I ended the &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;last post&lt;/a&gt; promising to show how a Rust command registers itself when the language flatly refuses to run any of your code before &lt;code&gt;main()&lt;/code&gt;. This is that post, and it&amp;rsquo;s a lovely example of reaching the same outcome by a completely different road.&lt;/p&gt;
&lt;p&gt;The outcome I wanted to keep is self-registration.&lt;/p&gt;
&lt;h2 id="what-self-registration-buys"&gt;What self-registration buys
&lt;/h2&gt;&lt;p&gt;A command in go-tool-base lives in its own file, and that file puts the command into the framework itself. There&amp;rsquo;s no central list of commands to keep in sync. You add a file, the command appears. You delete the file, it&amp;rsquo;s gone. Nothing else changes.&lt;/p&gt;
&lt;p&gt;That property is worth protecting. The alternative, a hand-maintained registry that every new command has to be threaded into, is exactly the sort of central file that turns into a merge-conflict magnet and quietly falls out of date. So when go-tool-base moved to Rust, self-registration was firmly in the column of things that had to survive.&lt;/p&gt;
&lt;p&gt;The way Go &lt;em&gt;did&lt;/em&gt; it was not.&lt;/p&gt;
&lt;h2 id="how-go-does-it"&gt;How Go does it
&lt;/h2&gt;&lt;p&gt;A Go package can declare an &lt;code&gt;init()&lt;/code&gt; function, and the runtime guarantees every &lt;code&gt;init()&lt;/code&gt; runs before &lt;code&gt;main()&lt;/code&gt; starts. A go-tool-base command file uses this to append itself to a package-level slice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;DeployCommand&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By the time &lt;code&gt;main()&lt;/code&gt; runs, every command file&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; has already fired and the registry slice is populated. It&amp;rsquo;s a tidy trick, and it leans entirely on a Go feature: code that executes before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="rust-doesnt-have-that"&gt;Rust doesn&amp;rsquo;t have that
&lt;/h2&gt;&lt;p&gt;Rust has no &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s no language-blessed phase that runs your code before &lt;code&gt;main()&lt;/code&gt;. This is a deliberate decision, not an oversight. Code running before &lt;code&gt;main()&lt;/code&gt; across many files has no well-defined order, and a startup phase whose ordering you can&amp;rsquo;t see is a classic source of subtle, miserable bugs. Rust closed that door on purpose.&lt;/p&gt;
&lt;p&gt;Which leaves a real question. If nothing runs before &lt;code&gt;main()&lt;/code&gt;, how does a command file insert itself into a registry without a central list editing it in?&lt;/p&gt;
&lt;h2 id="distributed-slices"&gt;Distributed slices
&lt;/h2&gt;&lt;p&gt;The answer is a crate called &lt;code&gt;linkme&lt;/code&gt;, and the mechanism is the &lt;em&gt;linker&lt;/em&gt; rather than a runtime phase.&lt;/p&gt;
&lt;p&gt;You declare a slice the framework will collect into:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;BUILTIN_COMMANDS&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A command file then contributes one entry to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice(BUILTIN_COMMANDS)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;register_greet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes it work. The &lt;code&gt;#[distributed_slice]&lt;/code&gt; attribute doesn&amp;rsquo;t generate any code that runs at startup. It places each entry into a dedicated section of the compiled object file. When the linker builds the final binary, it gathers everything in that section and lays it out as one contiguous array. &lt;code&gt;BUILTIN_COMMANDS&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; that array.&lt;/p&gt;
&lt;p&gt;So by the time the program exists as a binary on disk, the registry is already assembled. &lt;code&gt;main()&lt;/code&gt; doesn&amp;rsquo;t build it. No &lt;code&gt;init()&lt;/code&gt; builds it. The linker built it, statically, as part of producing the executable. At runtime the framework iterates a slice that was complete before the process ever started.&lt;/p&gt;
&lt;h2 id="what-you-get-from-it"&gt;What you get from it
&lt;/h2&gt;&lt;p&gt;The outcome is the one Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; gave, and then a bit more.&lt;/p&gt;
&lt;p&gt;A command still lives in one file and still self-registers. Adding a command is still adding a file. There&amp;rsquo;s still no central list.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s no startup phase to reason about, because there isn&amp;rsquo;t one. There&amp;rsquo;s no global mutable slice being appended to as &lt;code&gt;init()&lt;/code&gt;s fire, because nothing is appended at runtime; the slice is immutable and finished. There&amp;rsquo;s no ordering question, because the linker isn&amp;rsquo;t running your code, it&amp;rsquo;s collecting data. And it costs nothing at runtime: assembling the registry happened at link time, so program start just reads it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same idea go-tool-base had, expressed by the tool Rust actually gives you. Go reaches the registry through a controlled phase before &lt;code&gt;main()&lt;/code&gt;. Rust reaches it without any phase at all, because the linker did the assembly while the binary was still being built. Two roads, one destination&amp;hellip; which, if you&amp;rsquo;ve been following along, is becoming the whole theme of the Rust side of this project.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;Self-registration, where a command file inserts itself into the framework with no central list, is a property worth keeping. go-tool-base achieves it with a package-level &lt;code&gt;init()&lt;/code&gt;, leaning on Go&amp;rsquo;s guarantee that such functions run before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rust has no equivalent and wants none, because code running before &lt;code&gt;main()&lt;/code&gt; has no clear ordering. rust-tool-base uses &lt;code&gt;linkme&lt;/code&gt; distributed slices instead: each command is placed into a dedicated linker section, and the linker assembles them into one contiguous, immutable slice as it builds the binary. The registry is complete before the program runs. Same outcome as Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;, with no life before &lt;code&gt;main&lt;/code&gt; required.&lt;/p&gt;</description></item><item><title>Verifying your own downloads: how I solved it for self-updating CLI tools</title><link>https://blog-570662.gitlab.io/verifying-your-own-downloads/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/verifying-your-own-downloads/</guid><description>&lt;img src="https://blog-570662.gitlab.io/verifying-your-own-downloads/cover-verifying-your-own-downloads.png" alt="Featured image of post Verifying your own downloads: how I solved it for self-updating CLI tools" /&gt;&lt;p&gt;Way back in the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduction&lt;/a&gt; I promised I&amp;rsquo;d come back to the self-update integrity checks. Here we are. And the honest starting point is a slightly uncomfortable admission: for a good long while, go-tool-base&amp;rsquo;s &lt;code&gt;update&lt;/code&gt; command was the most trusting line of code in the entire tool.&lt;/p&gt;
&lt;h2 id="the-most-trusting-line-of-code-in-the-tool"&gt;The most trusting line of code in the tool
&lt;/h2&gt;&lt;p&gt;Self-update is a lovely feature. The user runs &lt;code&gt;yourtool update&lt;/code&gt;, the tool fetches the latest release, swaps itself out, and they&amp;rsquo;re current. go-tool-base has had this since early on, wired to GitHub, GitLab, Bitbucket, Gitea and a few others.&lt;/p&gt;
&lt;p&gt;But look closely at what that feature actually does. It reaches out to the internet, pulls down a file, and then &lt;em&gt;replaces the executable that&amp;rsquo;s currently running with that file&lt;/em&gt;. The next time the user invokes the tool, they&amp;rsquo;re running whatever those bytes turned out to be.&lt;/p&gt;
&lt;p&gt;The original implementation downloaded the release asset over HTTPS and extracted it. HTTPS gets you transport security: the bytes weren&amp;rsquo;t tampered with &lt;em&gt;in flight&lt;/em&gt;. It tells you nothing about whether the bytes were right when they left, or whether they&amp;rsquo;re even the bytes you meant to fetch. A truncated download, a CDN cache serving a mangled object, a release asset that got swapped after the fact&amp;hellip; HTTPS waves all of those straight through. For the one operation in the whole tool that replaces the binary, &amp;ldquo;we didn&amp;rsquo;t check&amp;rdquo; is an uncomfortable place to be sitting.&lt;/p&gt;
&lt;h2 id="goreleaser-already-does-half-the-job"&gt;GoReleaser already does half the job
&lt;/h2&gt;&lt;p&gt;The good news is that the build side was already producing exactly what I needed. GoReleaser, which builds go-tool-base&amp;rsquo;s releases, generates a &lt;code&gt;checksums.txt&lt;/code&gt; for every release: one SHA-256 per published artefact, the same format &lt;code&gt;sha256sum&lt;/code&gt; emits. It was sitting right there as a release asset and nothing was reading it.&lt;/p&gt;
&lt;p&gt;So Phase 1 of the integrity work is exactly that: read it.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;update&lt;/code&gt; downloads the platform binary, it now also fetches &lt;code&gt;checksums.txt&lt;/code&gt; from the same release, looks up the entry for the asset it just pulled, and compares the SHA-256 of the downloaded bytes against the expected hash before anything gets extracted or installed. Mismatch, and the update aborts before it has so much as touched the installed binary. The hash comparison runs in constant time, which is more defence-in-depth than strictly necessary here, but it costs nothing and means every hash comparison in the codebase is the same and reassuringly audit-boring.&lt;/p&gt;
&lt;h2 id="fail-open-or-fail-closed"&gt;Fail open, or fail closed?
&lt;/h2&gt;&lt;p&gt;The interesting design question wasn&amp;rsquo;t the hashing. It was: what do you do when there &lt;em&gt;is no&lt;/em&gt; &lt;code&gt;checksums.txt&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Plenty of older releases predate this feature. A release might have been cut by hand without GoReleaser. If go-tool-base flatly refused to update whenever a manifest was missing, the very act of shipping this feature would brick the update path for every existing tool the moment they upgraded into it. That&amp;rsquo;s a cure worse than the disease.&lt;/p&gt;
&lt;p&gt;So the default is fail-open: no manifest, log a clear warning, proceed. It matches how the existing offline-update path already behaved with its optional &lt;code&gt;.sha256&lt;/code&gt; sidecar, and it keeps upgrades working.&lt;/p&gt;
&lt;p&gt;Fail-open as a &lt;em&gt;default&lt;/em&gt; is not the same as fail-open being &lt;em&gt;right for everyone&lt;/em&gt;, though. A security-sensitive tool should be able to say &amp;ldquo;no manifest, no update, full stop&amp;rdquo;. Two ways to get there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool authors&lt;/strong&gt; flip a compile-time switch (&lt;code&gt;setup.DefaultRequireChecksum = true&lt;/code&gt; in &lt;code&gt;main()&lt;/code&gt;) and their binary ships fail-closed from day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End users&lt;/strong&gt; override either way through config (&lt;code&gt;update.require_checksum&lt;/code&gt;) or an environment variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;go-tool-base itself ships with the strict setting turned on, because a tool whose entire job is being a careful framework should hold itself to the stricter bar.&lt;/p&gt;
&lt;h2 id="the-honest-caveat"&gt;The honest caveat
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part I want to be straight about, because security features oversell themselves constantly.&lt;/p&gt;
&lt;p&gt;A checksum hosted &lt;em&gt;next to&lt;/em&gt; the binary it describes protects you from accidents. Corruption, truncation, a CDN serving stale junk, a release asset that got partially clobbered. It does not protect you from a determined attacker who&amp;rsquo;s compromised the release platform itself. If someone can replace the binary, they can replace &lt;code&gt;checksums.txt&lt;/code&gt; in the same breath, and your tool will cheerfully verify a malicious download against a malicious manifest and pronounce it good.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not a flaw in the implementation. It&amp;rsquo;s the inherent ceiling of &lt;em&gt;same-origin&lt;/em&gt; integrity: the manifest and the artefact share a trust root, so they fall together. Closing that gap needs a signature whose trust root is somewhere the release platform can&amp;rsquo;t reach, a key the attacker doesn&amp;rsquo;t have. That&amp;rsquo;s the next phase of this work, and it&amp;rsquo;s a bigger piece: &lt;a class="link" href="https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/" &gt;GPG-signing the manifest&lt;/a&gt;, with the public half both embedded in the binary and published independently so a single platform compromise isn&amp;rsquo;t enough.&lt;/p&gt;
&lt;p&gt;Phase 1 is the floor, not the ceiling. But it&amp;rsquo;s a floor worth having, because the overwhelming majority of real-world &amp;ldquo;the download was wrong&amp;rdquo; incidents are accidents, not attacks, and accidents are exactly what a same-origin checksum catches.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;update&lt;/code&gt; command is the most trusting code in a self-updating tool: it fetches bytes from the internet and then becomes them. go-tool-base now verifies the SHA-256 of every self-update download against the release&amp;rsquo;s own &lt;code&gt;checksums.txt&lt;/code&gt; before installing. It fails open by default so shipping the feature doesn&amp;rsquo;t strand anyone on an un-updatable version, fails closed for tool authors who ask (go-tool-base itself does), and stays honest that a same-origin checksum stops accidents, not a platform compromise.&lt;/p&gt;
&lt;p&gt;Verifying your own downloads is a low bar. The point is that the previous height of that bar was zero.&lt;/p&gt;</description></item><item><title>What survives a port, and what doesn't</title><link>https://blog-570662.gitlab.io/what-survives-a-port/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/what-survives-a-port/</guid><description>&lt;img src="https://blog-570662.gitlab.io/what-survives-a-port/cover-what-survives-a-port.png" alt="Featured image of post What survives a port, and what doesn't" /&gt;&lt;p&gt;Rebuilding go-tool-base in Rust turned out to be the most honest design review I&amp;rsquo;ve ever sat through, and I didn&amp;rsquo;t have to do anything except keep going. Porting a framework into a language with completely different idioms forces a separation you can&amp;rsquo;t fake: the parts that survive the move are &lt;em&gt;design&lt;/em&gt;, and the parts that don&amp;rsquo;t are just &lt;em&gt;habit&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="two-columns"&gt;Two columns
&lt;/h2&gt;&lt;p&gt;When you port a system between languages that don&amp;rsquo;t share idioms, every piece of it sorts itself into one of two columns, without you having to make the call.&lt;/p&gt;
&lt;p&gt;In the first column is the &lt;em&gt;outcome&lt;/em&gt; a piece of the design produces: every command receives the framework&amp;rsquo;s services, configuration is layered with a fixed precedence, commands register themselves, errors carry guidance to the user. In the second column is the &lt;em&gt;mechanism&lt;/em&gt; that produced that outcome in the original language.&lt;/p&gt;
&lt;p&gt;Things in the first column survive the port. You rebuild them, differently, because the tool genuinely needs them. Things in the second column do not survive. You find their replacement, and the Go version turns out to have been one valid implementation of an idea, not the idea itself. Doing this for go-tool-base, mechanism by mechanism, was more honest about my own design than any amount of sitting and staring at it would have been.&lt;/p&gt;
&lt;h2 id="the-container"&gt;The container
&lt;/h2&gt;&lt;p&gt;go-tool-base hands every command a &lt;code&gt;Props&lt;/code&gt; struct. It carries the logger, the config, the assets, the filesystem handle. Some of it is reached through loosely-typed accessors. It works well, and I &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;wrote a whole post about it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;outcome&lt;/em&gt; is column one: a command should receive one object, and that object should carry the framework&amp;rsquo;s services so the command doesn&amp;rsquo;t go assembling them itself. That survived. RTB hands every command an &lt;code&gt;App&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The loosely-typed accessors were column two. In Rust an &lt;code&gt;App&lt;/code&gt; is a plain struct with concrete fields, each one an &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;/code&gt; so a clone is a few atomic increments rather than a deep copy. Nothing is keyed by string. Nothing is fetched by name and asserted to a type. The thing the container &lt;em&gt;is for&lt;/em&gt; survived; the way Go expressed it did not.&lt;/p&gt;
&lt;h2 id="registration"&gt;Registration
&lt;/h2&gt;&lt;p&gt;A go-tool-base command self-registers using a package-level &lt;code&gt;init()&lt;/code&gt; function, which Go runs before &lt;code&gt;main()&lt;/code&gt; and which appends the command to a global slice.&lt;/p&gt;
&lt;p&gt;The outcome, column one, is that a command lives in its own file and inserts itself into the framework with no central list to edit. That&amp;rsquo;s genuinely worth keeping.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;init()&lt;/code&gt; mechanism is column two, and Rust doesn&amp;rsquo;t even offer it: Rust deliberately has no code that runs before &lt;code&gt;main()&lt;/code&gt;. The replacement is link-time registration through distributed slices, which gets its &lt;a class="link" href="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/" &gt;own post next&lt;/a&gt;. Same outcome, no global mutable state, assembled by the linker rather than by a startup phase.&lt;/p&gt;
&lt;h2 id="configuration"&gt;Configuration
&lt;/h2&gt;&lt;p&gt;go-tool-base layers configuration with a precedence: flags over environment over file over defaults. Some of it is read back through key lookups.&lt;/p&gt;
&lt;p&gt;The layering and the precedence are column one. They survived exactly. RTB layers config with the same ordering.&lt;/p&gt;
&lt;p&gt;The key lookups were column two. In Rust the merged configuration is deserialised into &lt;em&gt;your own&lt;/em&gt; &lt;code&gt;serde&lt;/code&gt; struct, so a config value is a typed field you access like any other field, and a typo is a compile error instead of a missing key at runtime. The precedence survived; reading values back out of a string-keyed bag did not.&lt;/p&gt;
&lt;h2 id="the-error-path"&gt;The error path
&lt;/h2&gt;&lt;p&gt;go-tool-base routes every error through one handler so presentation is consistent, which I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;also wrote up&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One consistent exit for errors is column one. It survived. What didn&amp;rsquo;t survive was the &lt;em&gt;handler&lt;/em&gt;: RTB has no error-handler object at all, because Rust&amp;rsquo;s own return-from-&lt;code&gt;main&lt;/code&gt; convention plus a report hook does the job the handler was built to do. That one has &lt;a class="link" href="https://blog-570662.gitlab.io/errors-without-an-error-handler/" &gt;its own post too&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-the-exercise-was-actually-worth"&gt;What the exercise was actually worth
&lt;/h2&gt;&lt;p&gt;Every mechanism told the same story. The container, the registration, the config access, the error path, the cancellation signal that go-tool-base carries on a &lt;code&gt;context.Context&lt;/code&gt; and RTB carries on a &lt;code&gt;CancellationToken&lt;/code&gt;. In every case the &lt;em&gt;thing it achieved&lt;/em&gt; walked across to Rust untouched, and the &lt;em&gt;Go code that achieved it&lt;/em&gt; was left behind.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the useful result. Before this port I couldn&amp;rsquo;t have told you, for any given pattern in go-tool-base, whether it was load-bearing design or just the idiomatic Go way to write it that day. Now I can, because each one was forced to prove itself by being rebuilt from nothing in a language that flatly wouldn&amp;rsquo;t accept the original. Whatever survived was real. Whatever I had to replace was always replaceable, which means it was never really the point.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Porting a framework into a language with different idioms separates design from habit for free. The outcome a pattern produces is design, and it survives the move. The mechanism that produced it is idiom, and it gets left behind for the new language&amp;rsquo;s equivalent.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;Props&lt;/code&gt; bag, its &lt;code&gt;init()&lt;/code&gt; registration, its key-based config access and its error handler were all idiom. The single context object, self-registration, layered precedence and a consistent error exit were all design, and all four came through to RTB intact. The next three posts take the most interesting replacements one at a time, starting with how a Rust command registers itself when the language won&amp;rsquo;t run anything before &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>rust-tool-base: the same idea, in a language that argues back</title><link>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</guid><description>&lt;img src="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/cover-rust-tool-base-the-same-idea.png" alt="Featured image of post rust-tool-base: the same idea, in a language that argues back" /&gt;&lt;p&gt;I built &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt; because I was sick of rebuilding the same CLI scaffolding every time I started a new Go tool. You&amp;rsquo;d think that would have taught me a lesson about doing things more than once. Apparently not, because I&amp;rsquo;ve now started building rust-tool-base: the same idea, the same itch, for Rust.&lt;/p&gt;
&lt;p&gt;In my defence, there&amp;rsquo;s method in it.&lt;/p&gt;
&lt;h2 id="the-same-itch-a-different-language"&gt;The same itch, a different language
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I kept writing the same couple of hundred lines of wiring every time I started a new Go CLI. Config loading, logging setup, an update check, an error path, a help system. None of it was the tool. All of it had to be there before the tool could be.&lt;/p&gt;
&lt;p&gt;Lately I&amp;rsquo;ve been learning Rust, and two things collided. The first is how I tend to learn a language. I&amp;rsquo;ve always picked them up reasonably quickly, and the way I do it isn&amp;rsquo;t with a tutorial that builds a toy, it&amp;rsquo;s by rebuilding something whose shape I already know cold, so that every decision is about &lt;em&gt;the language&lt;/em&gt; rather than &lt;em&gt;the problem&lt;/em&gt;. The second is that every time I started a Rust CLI of any size, I hit the very same gap I&amp;rsquo;d already filled once in Go.&lt;/p&gt;
&lt;p&gt;So rather than learn Rust on a throwaway, I decided to learn it by building rust-tool-base: the same idea, the same niche, for Rust.&lt;/p&gt;
&lt;h2 id="the-gap-in-rust"&gt;The gap in Rust
&lt;/h2&gt;&lt;p&gt;The Rust ecosystem has a well-earned reputation for sharp, focused crates and a deliberate shortage of big opinionated frameworks. &lt;code&gt;clap&lt;/code&gt; for argument parsing, &lt;code&gt;figment&lt;/code&gt; for layered config, &lt;code&gt;tracing&lt;/code&gt; for logging, &lt;code&gt;miette&lt;/code&gt; for errors, &lt;code&gt;ratatui&lt;/code&gt; for terminal UI, &lt;code&gt;reqwest&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt; underneath. Each of them is genuinely best-in-class.&lt;/p&gt;
&lt;p&gt;What nobody hands you is the assembly. Wiring those into one coherent product, and then adding self-update, AI integration, an MCP server, embedded documentation, credential handling, telemetry and a scaffolder, is real work, and it&amp;rsquo;s the same work on every project.&lt;/p&gt;
&lt;p&gt;The closest existing neighbours stop short of it. &lt;code&gt;cli-batteries&lt;/code&gt; is a thin preamble: argument parsing plus a logging subscriber plus panic and signal handling. &lt;code&gt;starbase&lt;/code&gt; has a proper session and lifecycle model but is CLI-agnostic and shaped around the moonrepo tooling it came from. &lt;code&gt;cargo-dist&lt;/code&gt; and &lt;code&gt;cargo-release&lt;/code&gt; are about release packaging, not the runtime. Good tools, all of them, but none is the opinionated, full-lifecycle, scaffolded base that go-tool-base is in the Go world. That space is empty, and rust-tool-base is built to fill it.&lt;/p&gt;
&lt;h2 id="why-it-is-not-a-port"&gt;Why it is not a port
&lt;/h2&gt;&lt;p&gt;The obvious way to build this would be to open go-tool-base and translate it file by file. I&amp;rsquo;m not doing that, and the reason matters enough that it&amp;rsquo;s the rule the whole project is built around.&lt;/p&gt;
&lt;p&gt;go-tool-base is full of Go. It leans on a &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt; that carries the framework&amp;rsquo;s services in loosely-typed fields. It configures things with functional options. It registers commands using package-level &lt;code&gt;init()&lt;/code&gt;. It threads a &lt;code&gt;context.Context&lt;/code&gt; through every call. Those are all good, idiomatic Go. Transliterated into Rust they&amp;rsquo;d become code that argues with the compiler on every single line, because Rust has its own answers to every one of those problems and they are emphatically not the Go answers.&lt;/p&gt;
&lt;p&gt;So rust-tool-base reaches the &lt;em&gt;same outcomes&lt;/em&gt; by Rust&amp;rsquo;s means. Commands still self-register, but through link-time machinery instead of &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s still one context object per command, but it&amp;rsquo;s strongly typed rather than a loosely-keyed bag. Configuration is still layered, but it lands in your own typed struct instead of a string-keyed lookup. Same philosophy, same shape of product, an entirely different ecosystem underneath. The README says it plainly: it&amp;rsquo;s a sibling, not a port.&lt;/p&gt;
&lt;h2 id="why-do-it-twice-at-all"&gt;Why do it twice at all
&lt;/h2&gt;&lt;p&gt;Three reasons, and they reinforce each other.&lt;/p&gt;
&lt;p&gt;The first is plain usefulness. The next time I want a Rust CLI tool, I want the same head start go-tool-base already gives me in Go.&lt;/p&gt;
&lt;p&gt;The second is the learning. Rebuilding a system I understand forces me to meet Rust&amp;rsquo;s idioms where they actually bite, not where a tutorial gently stages them. You learn ownership properly when a real design is pushing back at you.&lt;/p&gt;
&lt;p&gt;The third is the one I didn&amp;rsquo;t expect, and it&amp;rsquo;s the subject of the next post. Building the same framework twice, in two languages, turns out to be the cleanest way to find out which of your original decisions were genuine &lt;em&gt;design&lt;/em&gt; and which were merely &lt;em&gt;idiom&lt;/em&gt;. The design survives the move. The idiom does not. Sorting one from the other has been the most interesting part so far.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;rust-tool-base is the Rust sibling of go-tool-base: the same batteries-included, scaffolded, opinionated CLI framework, aimed at the same gap, which in Rust is the gap between a pile of excellent crates and a coherent product.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a port. Transliterating Go idioms into Rust produces code that fights the language, so RTB reaches the same outcomes through Rust&amp;rsquo;s own mechanisms instead. The posts after this one walk through the specific cases: how commands register, how the builder works, how errors are reported, and a few things RTB can do that the Go version structurally can&amp;rsquo;t. First, though, the thing the exercise taught me about my own design.&lt;/p&gt;</description></item><item><title>The blank import that keeps a dependency out of your binary</title><link>https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/cover-the-blank-import-that-keeps-a-dependency-out-of-your-binary.png" alt="Featured image of post The blank import that keeps a dependency out of your binary" /&gt;&lt;p&gt;go-tool-base can stash your credentials in the OS keychain, which most people building on it are perfectly happy about. But some of them ship into regulated and air-gapped environments where the binary isn&amp;rsquo;t &lt;em&gt;permitted&lt;/em&gt; to contain keychain or session-bus code at all&amp;hellip; not dormant, not unused, simply not there.&lt;/p&gt;
&lt;p&gt;So I had a feature most users want and a minority must be able to provably not have. The way I ended up solving it is one of my favourite little bits of honest Go.&lt;/p&gt;
&lt;h2 id="a-feature-some-users-have-to-be-able-to-not-have"&gt;A feature some users have to be able to &lt;em&gt;not have&lt;/em&gt;
&lt;/h2&gt;&lt;p&gt;go-tool-base needs somewhere to keep secrets: AI provider keys, VCS tokens, the occasional app password. The best home for those on a developer&amp;rsquo;s machine is the operating system&amp;rsquo;s own keychain. macOS Keychain, GNOME Keyring or KWallet on Linux via the Secret Service, Windows Credential Manager. So I wanted go-tool-base to support all three. (This is the keychain mode I mentioned back in the &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;credentials post&lt;/a&gt;, finally getting the explanation I promised it.)&lt;/p&gt;
&lt;p&gt;The Go library for that is &lt;a class="link" href="https://github.com/zalando/go-keyring" target="_blank" rel="noopener"
 &gt;&lt;code&gt;go-keyring&lt;/code&gt;&lt;/a&gt;, and it&amp;rsquo;s good. The catch is what it drags in behind it. On Linux it talks to the Secret Service over D-Bus, which means &lt;code&gt;godbus&lt;/code&gt;. On Windows it pulls &lt;code&gt;wincred&lt;/code&gt;. Perfectly reasonable dependencies for a desktop tool.&lt;/p&gt;
&lt;p&gt;Now here&amp;rsquo;s the constraint that made this interesting. Some of the people building tools on go-tool-base don&amp;rsquo;t ship to developer laptops. They ship into regulated sectors and air-gapped deployments where a security review will scan the binary, enumerate every dependency, and ask pointed questions about anything that does inter-process communication. For those builds, &amp;ldquo;the keychain code is there but we never call it&amp;rdquo; is not an acceptable answer. The reviewer&amp;rsquo;s position, and it&amp;rsquo;s a fair one, is that code which isn&amp;rsquo;t in the binary cannot be a finding.&lt;/p&gt;
&lt;p&gt;So I had a feature that most users want, and a minority of users must be able to provably &lt;em&gt;not have&lt;/em&gt;. Same framework, same release.&lt;/p&gt;
&lt;h2 id="why-i-didnt-reach-for-a-build-tag"&gt;Why I didn&amp;rsquo;t reach for a build tag
&lt;/h2&gt;&lt;p&gt;The obvious Go answer is a build tag. Compile with &lt;code&gt;-tags keychain&lt;/code&gt; to get it, leave the tag off to not. I started down that road. I even spent a while on an inverted version, a &lt;code&gt;nokeychain&lt;/code&gt; tag, on the theory that the regulated build should be the one that has to ask, so a forgotten flag fails safe.&lt;/p&gt;
&lt;p&gt;It works. It also isn&amp;rsquo;t very nice. Build tags are invisible at the call site. Nothing in the source tells you that a file only exists in some builds. The two worlds drift, because the tagged-out path isn&amp;rsquo;t compiled in your normal editor session and quietly rots. And the ergonomics for a &lt;em&gt;downstream consumer&lt;/em&gt; are poor: every tool built on go-tool-base would have to know the right magic incantation and thread it through their own release pipeline correctly, forever.&lt;/p&gt;
&lt;p&gt;I tried a second approach too: pull the keychain backend out into a completely separate Go module. That genuinely solves the dependency question (a module you don&amp;rsquo;t require can&amp;rsquo;t contribute to your &lt;code&gt;go.sum&lt;/code&gt;). But a separate module for one backend is clunky. Separate versioning, separate release, separate repo, all for a single file&amp;rsquo;s worth of behaviour. It felt like using a shipping container to post a letter.&lt;/p&gt;
&lt;h2 id="the-shape-that-actually-fits-a-registry-and-an-init"&gt;The shape that actually fits: a registry and an &lt;code&gt;init()&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The version I&amp;rsquo;m happy with leans on two boring, well-worn Go mechanisms and lets them do something quietly clever together.&lt;/p&gt;
&lt;p&gt;First, &lt;code&gt;pkg/credentials&lt;/code&gt; defines a &lt;code&gt;Backend&lt;/code&gt; interface and a registry. By default the registry holds a stub backend that politely returns &amp;ldquo;unsupported&amp;rdquo; for everything. The framework only ever talks to &lt;em&gt;the registered backend&lt;/em&gt;, whatever that happens to be.&lt;/p&gt;
&lt;p&gt;Second, the keychain implementation lives in its own package, &lt;code&gt;pkg/credentials/keychain&lt;/code&gt;, still inside the same module, no separate release to manage. That package has an &lt;code&gt;init()&lt;/code&gt; that registers its &lt;code&gt;go-keyring&lt;/code&gt;-backed backend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//nolint:gochecknoinits // registration via import is the whole point&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt;, &lt;code&gt;wincred&lt;/code&gt;, the whole IPC dependency chain, are only imported by &lt;em&gt;that&lt;/em&gt; package.&lt;/p&gt;
&lt;p&gt;Now the trick. To switch keychain support on, you import the package. You don&amp;rsquo;t have to &lt;em&gt;use&lt;/em&gt; anything from it. A blank import is enough, because a blank import still runs the package&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// cmd/gtb/keychain.go - the entire file.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/credentials/keychain&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That single line is the on/off switch for the shipped &lt;code&gt;gtb&lt;/code&gt; binary. The blank import means &lt;code&gt;init()&lt;/code&gt; runs, the keychain backend registers itself, and credential operations start routing through the OS keychain. No flag, no tag, no config.&lt;/p&gt;
&lt;h2 id="the-part-that-makes-it-provable"&gt;The part that makes it provable
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why this beats the build tag, and it comes down to one guarantee in the Go toolchain: &lt;strong&gt;the linker only includes packages that are actually imported.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;cmd/gtb/keychain.go&lt;/code&gt; exists, the &lt;code&gt;keychain&lt;/code&gt; package is in the import graph, so &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt; and &lt;code&gt;wincred&lt;/code&gt; are linked in. Delete that one file and rebuild, and the &lt;code&gt;keychain&lt;/code&gt; package is no longer reachable from &lt;code&gt;main&lt;/code&gt;. The linker performs dead-code elimination, and the entire &lt;code&gt;go-keyring&lt;/code&gt; chain is &lt;em&gt;gone&lt;/em&gt;. Not dormant. Not present-but-unused. Absent from the binary.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the bit a regulated build needs. It isn&amp;rsquo;t a promise that the code won&amp;rsquo;t run. It&amp;rsquo;s a structural fact that the code isn&amp;rsquo;t there, and you can hand a security reviewer an SBOM that proves it. &lt;code&gt;go-keyring&lt;/code&gt; won&amp;rsquo;t appear, because it genuinely isn&amp;rsquo;t linked.&lt;/p&gt;
&lt;p&gt;For a downstream tool built on go-tool-base the story is the same, and just as cheap. Want keychain support? Add the one-line blank import to your own &lt;code&gt;cmd&lt;/code&gt; package. Must ship keychain-free? Don&amp;rsquo;t. Your binary&amp;rsquo;s dependency graph follows your import graph, exactly as Go always promised it would. The default (no import) is the locked-down one, which is the right way round for a safety property.&lt;/p&gt;
&lt;h2 id="why-i-like-this-more-than-i-expected-to"&gt;Why I like this more than I expected to
&lt;/h2&gt;&lt;p&gt;Build tags hide a decision in the compiler invocation. This pattern puts the decision in the source, as an import, where it&amp;rsquo;s greppable, obvious in code review, and impossible to get subtly wrong. There&amp;rsquo;s a real file called &lt;code&gt;keychain.go&lt;/code&gt; whose entire content is one import, and it reads as exactly what it is: a switch.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also just &lt;em&gt;honest&lt;/em&gt; Go. No reflection, no plugin loader, no clever runtime. A registry, an &lt;code&gt;init()&lt;/code&gt;, and the linker doing the one job it&amp;rsquo;s always done. The cleverness, such as it is, is in the arrangement, not in any individual piece.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base needed OS keychain support for the many, and a way to provably exclude it for the few. Build tags could express the toggle but hid it in the build invocation and rotted in the dark. A separate module solved the dependency question but was far too much machinery for one backend.&lt;/p&gt;
&lt;p&gt;Putting the keychain backend in its own package, activated by a blank &lt;code&gt;import _&lt;/code&gt; that fires its &lt;code&gt;init()&lt;/code&gt;, gets you both: a one-line, in-source, code-reviewable switch, and, because the linker only links what&amp;rsquo;s imported, a build with the import omitted that contains &lt;em&gt;none&lt;/em&gt; of the keychain dependency chain. Provable absence, not promised disuse.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re carrying an optional dependency that some of your users need gone rather than merely idle, this is the pattern. Let the import graph be the feature flag.&lt;/p&gt;</description></item><item><title>Where should a CLI keep your API keys?</title><link>https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/</guid><description>&lt;img src="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/cover-where-should-a-cli-keep-your-api-keys.png" alt="Featured image of post Where should a CLI keep your API keys?" /&gt;&lt;p&gt;Your CLI tool needs the user&amp;rsquo;s API key. It has to come from somewhere, and it has to survive between runs, so the obvious move is to ask once and write it into the config file. One tidy &lt;code&gt;api_key:&lt;/code&gt; line. Job done.&lt;/p&gt;
&lt;p&gt;It works beautifully on the first afternoon. And then, months later, it&amp;rsquo;s quietly become a liability nobody actually decided to create.&lt;/p&gt;
&lt;h2 id="the-config-file-that-quietly-becomes-a-liability"&gt;The config file that quietly becomes a liability
&lt;/h2&gt;&lt;p&gt;Your CLI tool needs the user&amp;rsquo;s API key. It has to come from somewhere, and it has to survive between invocations, so the obvious move is to ask once and write it into the tool&amp;rsquo;s config file. &lt;code&gt;~/.config/yourtool/config.yaml&lt;/code&gt;, a nice &lt;code&gt;api_key:&lt;/code&gt; line, done.&lt;/p&gt;
&lt;p&gt;It works on the first afternoon. It keeps working. And then, slowly, it becomes a problem nobody decided to create.&lt;/p&gt;
&lt;p&gt;The config file gets committed to a dotfiles repo. It gets caught in a &lt;code&gt;tar&lt;/code&gt; of someone&amp;rsquo;s home directory that lands in a backup bucket. It scrolls past in a screen share. It sits, world-readable, on a shared build box. None of these are exotic. They&amp;rsquo;re just a Tuesday. The plaintext key was fine right up until the file went somewhere the key shouldn&amp;rsquo;t, and config files go places.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want go-tool-base handing every tool built on it that same slow-motion liability by default. So credential handling got rebuilt around a simple idea: the config file should usually hold a &lt;em&gt;reference&lt;/em&gt; to the secret, not the secret itself.&lt;/p&gt;
&lt;h2 id="three-modes-and-which-one-you-get"&gt;Three modes, and which one you get
&lt;/h2&gt;&lt;p&gt;go-tool-base supports three ways to store a credential.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment-variable reference, the default.&lt;/strong&gt; The config records the &lt;em&gt;name&lt;/em&gt; of an environment variable, not its value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The secret itself lives in your shell profile, your &lt;code&gt;direnv&lt;/code&gt; setup, or your CI platform&amp;rsquo;s secret store, wherever you already keep that sort of thing. The config file now contains nothing sensitive at all. You can commit it, back it up, paste it into a bug report. The reference is inert on its own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OS keychain, opt-in.&lt;/strong&gt; The config holds a &lt;code&gt;&amp;lt;service&amp;gt;/&amp;lt;account&amp;gt;&lt;/code&gt; reference and the actual secret goes into the operating system&amp;rsquo;s keychain: macOS Keychain, GNOME Keyring or KWallet via the Secret Service, Windows Credential Manager.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;keychain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mytool/anthropic.api&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one is opt-in by design, because the keychain backend carries dependencies that some deployments simply aren&amp;rsquo;t allowed to ship. (That opt-in mechanism turned out to be an interesting little problem all of its own, and it gets &lt;a class="link" href="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/" &gt;its own post&lt;/a&gt; in a couple of days.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Literal value, legacy and grudging.&lt;/strong&gt; The old behaviour. The secret sits in the config in plaintext:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sk-ant-...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It still works, because breaking every existing tool&amp;rsquo;s config on an upgrade would be its own kind of vandalism. But it&amp;rsquo;s the last resort, it&amp;rsquo;s documented as the last resort, and the setup wizard puts a warning in front of you when you pick it.&lt;/p&gt;
&lt;h2 id="the-one-place-literal-mode-is-not-allowed"&gt;The one place literal mode is not allowed
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a single hard &amp;ldquo;no&amp;rdquo; in all of this. If go-tool-base detects it&amp;rsquo;s running in CI (&lt;code&gt;CI=true&lt;/code&gt;, which every major CI platform sets) the setup flow will &lt;em&gt;refuse&lt;/em&gt; to write a literal credential, and exits non-zero.&lt;/p&gt;
&lt;p&gt;The reasoning is that a plaintext secret written during a CI run is a plaintext secret written onto an ephemeral, often shared, frequently-logged machine, by an automated process that no human is watching. That&amp;rsquo;s the exact situation where the slow-motion liability becomes a fast one. CI environments inject secrets as environment variables already; there&amp;rsquo;s no good reason for a tool to be writing one to disk there, so go-tool-base simply won&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="how-it-decides-at-runtime"&gt;How it decides at runtime
&lt;/h2&gt;&lt;p&gt;A credential can be configured more than one way at once. You might have an &lt;code&gt;env&lt;/code&gt; reference &lt;em&gt;and&lt;/em&gt; an old literal &lt;code&gt;key&lt;/code&gt; still lurking. So resolution follows a fixed precedence, highest to lowest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;*.env&lt;/code&gt; reference. If that env var is set, use it.&lt;/li&gt;
&lt;li&gt;Otherwise the &lt;code&gt;*.keychain&lt;/code&gt; reference. If a keychain entry resolves, use it.&lt;/li&gt;
&lt;li&gt;Otherwise the literal &lt;code&gt;*.key&lt;/code&gt; / &lt;code&gt;*.value&lt;/code&gt;, the legacy path.&lt;/li&gt;
&lt;li&gt;Otherwise a well-known fallback env var (&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; and friends), so a tool still picks up the ecosystem-standard variable with no config at all.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The useful property here is that adding a more secure mode &lt;em&gt;transparently wins&lt;/em&gt;. Drop an &lt;code&gt;env&lt;/code&gt; reference next to an old literal key and the next run uses the env var. You can migrate a credential to a better home without first removing it from its worse one, which makes the migration safe to do incrementally instead of as one nervous big-bang edit.&lt;/p&gt;
&lt;h2 id="the-tool-tells-on-itself"&gt;The tool tells on itself
&lt;/h2&gt;&lt;p&gt;A precedence rule is no use if nobody knows their config still has a plaintext key three layers down. So the built-in &lt;code&gt;doctor&lt;/code&gt; command grew a check for exactly that. Run &lt;code&gt;doctor&lt;/code&gt;, and if any literal credential is sitting in your config it reports a warning, names the offending keys (the key &lt;em&gt;names&lt;/em&gt;, never the values) and points you at how to migrate.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not an error. Literal mode is still legal. But the tool will quietly keep reminding you that you left the campsite messier than you could have, until you go and tidy it. (Old Scout habits die hard, and they&amp;rsquo;ve leaked all the way into the framework.)&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;A CLI tool that writes your API key into a plaintext config file isn&amp;rsquo;t doing anything &lt;em&gt;wrong&lt;/em&gt;, exactly. It&amp;rsquo;s just handing you a liability that activates later, when the file travels somewhere the key shouldn&amp;rsquo;t. go-tool-base&amp;rsquo;s answer is three storage modes: an env-var reference by default, the OS keychain on request, and a plaintext literal only as a documented last resort that CI environments can&amp;rsquo;t use at all. Runtime resolution runs in a fixed precedence so a more secure mode always wins, which makes migrating a credential safe to do gradually. And &lt;code&gt;doctor&lt;/code&gt; keeps an eye on the config so a stray plaintext secret doesn&amp;rsquo;t get to hide forever.&lt;/p&gt;
&lt;p&gt;The secret should live in a secret store. The config file should just know its name.&lt;/p&gt;</description></item><item><title>I had the framework audited: every finding was the same shape</title><link>https://blog-570662.gitlab.io/every-finding-was-the-same-shape/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/every-finding-was-the-same-shape/</guid><description>&lt;img src="https://blog-570662.gitlab.io/every-finding-was-the-same-shape/cover-every-finding-was-the-same-shape.png" alt="Featured image of post I had the framework audited: every finding was the same shape" /&gt;&lt;p&gt;When a real security audit lands back in your inbox, the temptation is to read it as a shopping list of unrelated mistakes. Fix one, fix the next, tick them off, move on. I did exactly that the first time. The second time, I noticed something far more useful: the findings weren&amp;rsquo;t scattered at all. They clustered. Almost every one was the same sentence with the nouns swapped out.&lt;/p&gt;
&lt;h2 id="findings-cluster-they-dont-scatter"&gt;Findings cluster, they don&amp;rsquo;t scatter
&lt;/h2&gt;&lt;p&gt;When you get a real security audit back, the instinct is to read it as a list of unrelated mistakes. Finding 1, unrelated to Finding 2, unrelated to Finding 3. Triage each, fix each, move on.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not what the go-tool-base audits looked like once I stopped reading them as a list. The findings &lt;em&gt;clustered&lt;/em&gt;. Strip away the specifics and almost every one was the same sentence with the nouns swapped: &lt;em&gt;untrusted input reaches a powerful operation, and nothing checks it in between.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That reframe is worth more than any individual fix, because it turns &amp;ldquo;we patched some bugs&amp;rdquo; into &amp;ldquo;we know where to look next time&amp;rdquo;. A framework&amp;rsquo;s attack surface isn&amp;rsquo;t spread evenly. It&amp;rsquo;s concentrated at the &lt;em&gt;boundaries&lt;/em&gt;: the handful of points where data from outside (a config file, a command-line flag, something typed into a TUI, an HTTP response) flows into machinery that can be made to misbehave. Audit the boundaries and you&amp;rsquo;ve audited most of the risk. Three examples make the pattern obvious.&lt;/p&gt;
&lt;h2 id="boundary-one-a-regex-compiler"&gt;Boundary one: a regex compiler
&lt;/h2&gt;&lt;p&gt;Somewhere in the tool, a user-supplied string gets compiled into a regular expression. A search pattern typed into the docs browser, a filter from a config file. Feeding user input to &lt;code&gt;regexp.Compile&lt;/code&gt; feels harmless. It&amp;rsquo;s just pattern matching, after all.&lt;/p&gt;
&lt;p&gt;It isn&amp;rsquo;t quite harmless. A regular expression is a tiny program, and some tiny programs are catastrophically slow. A pattern with the wrong kind of nested repetition can take exponential time to evaluate against a modestly hostile input. That&amp;rsquo;s the class of bug known as ReDoS. A user, or something feeding the user&amp;rsquo;s config, hands you a pathological pattern and your tool wedges, burning a whole core, on what looked for all the world like a search box.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t to ban user-supplied regexes. It&amp;rsquo;s to stop treating &amp;ldquo;compile this string&amp;rdquo; as free. go-tool-base routes any regex whose pattern came from outside the binary through a &lt;code&gt;regexutil.CompileBounded&lt;/code&gt; helper. It caps the pattern length and puts a hard timeout on compilation. A pattern known at build time can still use plain &lt;code&gt;regexp.MustCompile&lt;/code&gt;, because that isn&amp;rsquo;t a boundary, it&amp;rsquo;s a constant. The discipline only applies where the input genuinely crosses in.&lt;/p&gt;
&lt;h2 id="boundary-two-a-url-opener"&gt;Boundary two: a URL opener
&lt;/h2&gt;&lt;p&gt;The tool needs to open a URL in the user&amp;rsquo;s browser, a docs link or an OAuth flow. Under the hood that&amp;rsquo;s the OS handler: &lt;code&gt;xdg-open&lt;/code&gt;, or &lt;code&gt;open&lt;/code&gt;, or &lt;code&gt;rundll32&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now ask where the URL came from. If any part of it is influenced by config, by a server response, by user input, then &amp;ldquo;open this URL&amp;rdquo; has quietly become &amp;ldquo;ask the operating system to do something with an attacker-influenced string&amp;rdquo;. A &lt;code&gt;file://&lt;/code&gt; URL. A &lt;code&gt;javascript:&lt;/code&gt; URL. Something with control characters smuggled into it. The browser-open was never the dangerous part. The &lt;em&gt;unvalidated string&lt;/em&gt; was.&lt;/p&gt;
&lt;p&gt;So go-tool-base funnels every URL-open through one package, &lt;code&gt;pkg/browser&lt;/code&gt;, and that package is a gate. It enforces an allowlist of schemes (&lt;code&gt;https&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;mailto&lt;/code&gt;, and nothing else), bounds the length, and rejects control characters before the OS ever sees the string. The rule that makes it stick is that nothing else is allowed to call the OS handler directly. One door, and the door has a lock. A scattered capability with no chokepoint can&amp;rsquo;t be secured; a capability that &lt;em&gt;has&lt;/em&gt; a chokepoint can. (You&amp;rsquo;ll have spotted the &amp;ldquo;one door out&amp;rdquo; idea by now&amp;hellip; it&amp;rsquo;s the same instinct as the &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;single error handler&lt;/a&gt;, pointed at security instead of consistency.)&lt;/p&gt;
&lt;h2 id="boundary-three-a-log-sink"&gt;Boundary three: a log sink
&lt;/h2&gt;&lt;p&gt;This one&amp;rsquo;s the sneakiest, because it runs the wrong way round. The first two boundaries are about dangerous input coming &lt;em&gt;in&lt;/em&gt;. This one is about sensitive data leaking &lt;em&gt;out&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The tool handles credentials. It also logs, emits telemetry, and reports errors, and all three of those are &lt;em&gt;exit&lt;/em&gt; boundaries: places where strings leave the process for somewhere more persistent and more public, like a log aggregator, an analytics backend, an error tracker. If a token ever ends up in a string that flows to one of those, you haven&amp;rsquo;t logged an event, you&amp;rsquo;ve published a secret.&lt;/p&gt;
&lt;p&gt;The defence is &lt;code&gt;pkg/redact&lt;/code&gt;. Any free-form string heading for an observability surface goes through it first, and it strips the usual suspects: credentials in URL userinfo, sensitive query parameters, &lt;code&gt;Authorization&lt;/code&gt; headers, the well-known provider key prefixes (&lt;code&gt;sk-&lt;/code&gt;, &lt;code&gt;ghp_&lt;/code&gt;, &lt;code&gt;AIza&lt;/code&gt; and friends), long opaque tokens. The places most likely to leak, command arguments and error messages in telemetry, get it applied automatically rather than relying on every caller to remember.&lt;/p&gt;
&lt;p&gt;Same pattern as the other two. A boundary, and something standing on it checking what goes through.&lt;/p&gt;
&lt;h2 id="the-unglamorous-part"&gt;The unglamorous part
&lt;/h2&gt;&lt;p&gt;None of these fixes is clever. There&amp;rsquo;s no exploit demo, no neat trick to show off. Bound a length. Check a scheme against an allowlist. Run a string through a redactor. The work was almost entirely in &lt;em&gt;noticing the boundary existed&lt;/em&gt;, and then making sure everything routes through the one checked path instead of dotting raw calls all over the codebase.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the actual lesson of a security audit, and it&amp;rsquo;s why the cluster reframe matters. The value wasn&amp;rsquo;t the dozen-or-so individual fixes. It was learning that the next risk will be at a boundary too, the next place untrusted input meets a powerful operation with nothing in between, and that the job is to find those points and put a single, mandatory, checked door on each.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;A security audit of a CLI framework reads like a list of unrelated bugs and isn&amp;rsquo;t one. go-tool-base&amp;rsquo;s findings nearly all reduced to the same shape: untrusted input reaching a powerful operation unchecked. A regex compiler that needed a length and time bound (&lt;code&gt;regexutil.CompileBounded&lt;/code&gt;). A URL opener that needed a scheme allowlist and a single chokepoint (&lt;code&gt;pkg/browser&lt;/code&gt;). Log and telemetry sinks that needed credentials redacted on the way out (&lt;code&gt;pkg/redact&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The fixes were structural and dull, which is exactly right. Find your boundaries (config, flags, TUI input, network responses, log and telemetry sinks), give each one a single mandatory checked path, and you&amp;rsquo;ve spent your audit effort where the risk actually lives.&lt;/p&gt;</description></item><item><title>The test-mocking pattern that races</title><link>https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/</link><pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/cover-the-test-mocking-pattern-that-races.png" alt="Featured image of post The test-mocking pattern that races" /&gt;&lt;p&gt;I&amp;rsquo;m going to tell you about a bug go-tool-base shipped, because it&amp;rsquo;s one of those bugs that&amp;rsquo;s so reasonable-looking you&amp;rsquo;ll find it in textbooks, conference talks, and an awful lot of otherwise excellent Go code. We had it too. It passed every test on my laptop, every single time, and then quietly fell over on CI while blaming an innocent bystander.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the classic Go trick for mocking a dependency, and it races.&lt;/p&gt;
&lt;h2 id="a-pattern-that-looks-completely-reasonable"&gt;A pattern that looks completely reasonable
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s a thing you need to do constantly in Go tests: stop a function from really shelling out. It calls &lt;code&gt;exec.LookPath&lt;/code&gt; to find a binary, or &lt;code&gt;exec.Command&lt;/code&gt; to run one, and your test very much does not want it touching the real &lt;code&gt;$PATH&lt;/code&gt; or spawning a real process.&lt;/p&gt;
&lt;p&gt;The Go community has a well-worn answer. Hoist the function into a package-level variable, call &lt;em&gt;that&lt;/em&gt;, and let tests reassign it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// production code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;findTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;execLookPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sometool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// test&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TestFindTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/fake/path&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...assert...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s tidy. No interface to thread through, no constructor to change. You&amp;rsquo;ll find it in a great deal of Go code, including some very respectable Go code indeed. go-tool-base had it too.&lt;/p&gt;
&lt;p&gt;And it works. It works on your machine, it works in code review, it works the first hundred times CI runs it. Which is precisely what makes it dangerous, because it&amp;rsquo;s wrong, and it&amp;rsquo;s just been biding its time.&lt;/p&gt;
&lt;h2 id="add-one-line-and-it-detonates"&gt;Add one line and it detonates
&lt;/h2&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;t.Parallel()&lt;/code&gt; is more or less free performance. Mark your tests with it and the runner overlaps them instead of plodding through one at a time. On a package with a few hundred tests it&amp;rsquo;s a real, worthwhile speed-up, so naturally you reach for it.&lt;/p&gt;
&lt;p&gt;Now picture two tests, both using the pattern above, both marked &lt;code&gt;t.Parallel()&lt;/code&gt;. They run concurrently. Test A assigns its fake to &lt;code&gt;execLookPath&lt;/code&gt;. Test B assigns &lt;em&gt;its&lt;/em&gt; fake to &lt;code&gt;execLookPath&lt;/code&gt;. Test A reads &lt;code&gt;execLookPath&lt;/code&gt; expecting its own fake. Two goroutines, one variable, writes and reads with nothing synchronising them. That&amp;rsquo;s a textbook data race, and the textbook is right: the behaviour is undefined. Test A might see B&amp;rsquo;s fake. The deferred restore might land in the wrong order and leave the variable pointing at a fake after both tests have finished, poisoning a third one for good measure.&lt;/p&gt;
&lt;p&gt;The truly nasty part is the &lt;em&gt;intermittency&lt;/em&gt;. Whether the race actually bites depends on goroutine scheduling, which depends on machine load and core count. Your laptop running eight tests at once might never lose the coin-toss. A CI runner under load, scheduling differently, loses it and fails a test that has nothing obviously to do with the change in the commit. You re-run the pipeline, it passes, everyone shrugs and moves on. A test suite that fails one run in twenty trains your team to ignore it, and an ignored CI failure is worse than no CI at all.&lt;/p&gt;
&lt;p&gt;I can tell you this one from direct, slightly embarrassed experience, because go-tool-base shipped exactly this bug and CI caught it the honest way: green on the laptop, red on the runner, with the failure cheerfully pointing at innocent bystander tests rather than the global that was actually the culprit. &lt;code&gt;go test -race&lt;/code&gt; will name it for you if you crank the parallelism up high enough to lose the toss reliably&amp;hellip; but you have to go looking, and you only go looking once it&amp;rsquo;s already ruined an afternoon.&lt;/p&gt;
&lt;h2 id="the-fix-isnt-synchronisation-its-structure"&gt;The fix isn&amp;rsquo;t synchronisation, it&amp;rsquo;s structure
&lt;/h2&gt;&lt;p&gt;The instinct is to slap a mutex around the variable. Resist it. A mutex makes the race &lt;em&gt;defined&lt;/em&gt;, but it doesn&amp;rsquo;t make the design any good. You&amp;rsquo;ve still got global mutable state, you&amp;rsquo;ve just queued the fight instead of cancelling it. And tests that serialise on a shared lock aren&amp;rsquo;t really parallel any more, so you&amp;rsquo;ve also handed back the speed-up you came for in the first place.&lt;/p&gt;
&lt;p&gt;The real fix is to not have a shared variable at all. The dependency was always an &lt;em&gt;input&lt;/em&gt; to the code; the package-level var was just a way of avoiding saying so out loud. So say it. Inject it.&lt;/p&gt;
&lt;p&gt;A struct field:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Finder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// defaults to exec.LookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Finder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sometool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or a functional option, if you&amp;rsquo;d rather keep the zero value clean. Either way, each test constructs its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;Finder&lt;/code&gt; with its &lt;em&gt;own&lt;/em&gt; fake. There&amp;rsquo;s no shared variable, so there&amp;rsquo;s no race, and &lt;code&gt;t.Parallel()&lt;/code&gt; is free again because the tests genuinely don&amp;rsquo;t touch each other.&lt;/p&gt;
&lt;p&gt;go-tool-base wrote this straight into its standing rules: no package-level mocking hooks, full stop. Dependencies come in through struct fields, functional options, or config fields. (The same injection discipline that makes &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt; so testable, applied one rung further down.) And to stop everyone hand-rolling the same &lt;code&gt;exec&lt;/code&gt; fakes, there&amp;rsquo;s a small internal package, &lt;code&gt;internal/exectest&lt;/code&gt;, with ready-made &lt;code&gt;LookPath&lt;/code&gt; and &lt;code&gt;CommandContext&lt;/code&gt; doubles you construct per-test. The pattern is gone, and the door it came in through is shut.&lt;/p&gt;
&lt;h2 id="the-rule-worth-taking-away"&gt;The rule worth taking away
&lt;/h2&gt;&lt;p&gt;A package-level variable that tests reassign is shared mutable state. It reads as a harmless convenience because in a single-threaded test run it behaves like one. &lt;code&gt;t.Parallel()&lt;/code&gt; is the thing that reveals it was never harmless, only unobserved.&lt;/p&gt;
&lt;p&gt;The general lesson is older than Go: &lt;strong&gt;if a value is an input to your code, make it an input.&lt;/strong&gt; Smuggling it in as a global is borrowing test-time convenience against a debt that comes due, with interest, the day someone wants their tests to run in parallel. Pay cash. Inject the dependency.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Mocking via a reassignable package-level variable is a beloved Go shortcut and a latent data race. It survives because single-threaded test runs hide it; &lt;code&gt;t.Parallel()&lt;/code&gt; exposes it as intermittent, bystander-blaming CI flake that&amp;rsquo;s miserable to trace. A mutex only makes the bad design &lt;em&gt;defined&lt;/em&gt;. The fix is structural: inject the dependency as a struct field or functional option, so each test owns its own double and there&amp;rsquo;s no shared state to race over. go-tool-base banned the global-hook pattern outright and ships &lt;code&gt;internal/exectest&lt;/code&gt; so nobody&amp;rsquo;s tempted back to it.&lt;/p&gt;
&lt;p&gt;If a piece of code depends on something, let it &lt;em&gt;say&lt;/em&gt; so in its signature. Your future self, staring at a CI failure that flatly refuses to reproduce, will thank you.&lt;/p&gt;</description></item><item><title>Testing code that calls an LLM: yes, you actually can</title><link>https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/</link><pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/</guid><description>&lt;img src="https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/cover-testing-code-that-calls-an-llm.png" alt="Featured image of post Testing code that calls an LLM: yes, you actually can" /&gt;&lt;p&gt;&amp;ldquo;You can&amp;rsquo;t test code that calls an AI.&amp;rdquo; I&amp;rsquo;ve heard it said with great confidence, and it&amp;rsquo;s half right, which is the most dangerous kind of right. You genuinely can&amp;rsquo;t assert on what a non-deterministic model says. But the model isn&amp;rsquo;t your code, and the bits sitting either side of it most certainly are.&lt;/p&gt;
&lt;h2 id="you-cant-test-ai-code"&gt;&amp;ldquo;You can&amp;rsquo;t test AI code&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s a fair worry. Your command calls an LLM. The LLM returns something slightly different every run. A test that asserts &lt;code&gt;response == &amp;quot;...&amp;quot;&lt;/code&gt; is broken before you&amp;rsquo;ve finished typing it. So the conclusion arrives quickly: the AI path can&amp;rsquo;t be tested, leave it uncovered.&lt;/p&gt;
&lt;p&gt;Which is a shame, because the AI call is usually the riskiest line in the whole command.&lt;/p&gt;
&lt;p&gt;The conclusion is also wrong. It mistakes &amp;ldquo;I can&amp;rsquo;t test the model&amp;rdquo; for &amp;ldquo;I can&amp;rsquo;t test my code&amp;rdquo;. The model is not your code. Your code is the two pieces sitting on either side of it.&lt;/p&gt;
&lt;h2 id="your-code-is-a-prompt-and-a-handler"&gt;Your code is a prompt and a handler
&lt;/h2&gt;&lt;p&gt;Strip the command down to what it actually does:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It builds a prompt. It assembles a system prompt, the user&amp;rsquo;s input, perhaps some context, and sends it.&lt;/li&gt;
&lt;li&gt;The model does something. This is not your code.&lt;/li&gt;
&lt;li&gt;It takes the response and does something with it. It parses it, branches on it, prints it, stores it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps one and three are entirely yours, and entirely deterministic. The same inputs build the same prompt and handle the same response the same way, every single time. That&amp;rsquo;s testable. Step two is the only part that isn&amp;rsquo;t, and step two was never yours to test in the first place.&lt;/p&gt;
&lt;p&gt;So the job is to pin step two to a known value, and then test one and three properly.&lt;/p&gt;
&lt;h2 id="test-the-prompt-snapshot-it"&gt;Test the prompt: snapshot it
&lt;/h2&gt;&lt;p&gt;Step one produces a prompt, and a prompt is just a string, which means you can pin it.&lt;/p&gt;
&lt;p&gt;Both frameworks lean on snapshot testing here. go-tool-base uses a golden-file approach: the prompt your code generates is recorded to a file, and the test re-generates it and compares against that file. rust-tool-base does the same with &lt;code&gt;insta&lt;/code&gt;, snapshotting the request body the client would send.&lt;/p&gt;
&lt;p&gt;The reason this matters is that the prompt is load-bearing and quietly easy to break. You refactor how context gets assembled. Without noticing, you&amp;rsquo;ve changed the wording, or the ordering, or dropped a line the model was leaning on. Nothing fails to compile. The behaviour just drifts, silently.&lt;/p&gt;
&lt;p&gt;A snapshot test catches exactly that. It fails, it shows you the diff between the old prompt and the new one, and it makes you stop and make a decision. Was this change intended? If yes, you accept the new snapshot and move on. If no, you&amp;rsquo;ve just caught a bug before it shipped. Either way the prompt never changes by accident, which for AI code is most of the battle.&lt;/p&gt;
&lt;h2 id="test-the-handler-mock-the-response"&gt;Test the handler: mock the response
&lt;/h2&gt;&lt;p&gt;Step three needs a response to handle, and in a unit test you don&amp;rsquo;t get that response from the real model. You supply it.&lt;/p&gt;
&lt;p&gt;go-tool-base ships generated mocks for the &lt;code&gt;ChatClient&lt;/code&gt; interface. A test builds a mock client, tells it &amp;ldquo;when &lt;code&gt;Ask&lt;/code&gt; is called, return &lt;em&gt;this&lt;/em&gt; canned value&amp;rdquo;, and runs the command against it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mockClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock_chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewMockChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EXPECT&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Anything&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Anything&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnythingOfType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;*main.Analysis&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunAndReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;critical&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the interface is only four methods&lt;/a&gt;, that mock is trivial to set up and complete by construction. rust-tool-base takes the same idea one layer down: HTTP-bound tests use &lt;code&gt;wiremock&lt;/code&gt;, which stands up a fake server returning a canned response body. The client makes a real HTTP request; it just goes to a fake endpoint the test controls.&lt;/p&gt;
&lt;p&gt;Either way, step two is now fixed to a value you chose, which makes step three deterministic. And that unlocks the tests that actually matter: given a malformed response, does the command fail gracefully? Given a rate-limit error, an empty answer, a field missing? Those are the cases a live model almost never hands you on demand, and a mock hands you every time, on the first run.&lt;/p&gt;
&lt;p&gt;This is, incidentally, the same discipline as &lt;a class="link" href="https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/" &gt;the test-mocking work elsewhere in the framework&lt;/a&gt;: the dependency is injected, so the test gets to decide what it does.&lt;/p&gt;
&lt;h2 id="what-you-deliberately-dont-test"&gt;What you deliberately don&amp;rsquo;t test
&lt;/h2&gt;&lt;p&gt;One honest boundary. None of this tests whether the model gives &lt;em&gt;good&lt;/em&gt; answers. That question is real, but it&amp;rsquo;s a different activity (evaluations, run as their own suite) and not something to mix into the unit tests.&lt;/p&gt;
&lt;p&gt;The unit suite&amp;rsquo;s job is your code: that it builds a sound prompt, and that it handles every shape of response correctly, including the ugly ones. Keep that well away from &amp;ldquo;is the model clever today&amp;rdquo;. A unit test that depends on the model being clever is a unit test that fails when the weather changes, and a flaky test just teaches people to ignore the whole suite.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;Code that calls an LLM is testable; the model is not, and those are different statements. Your code is a prompt builder and a response handler, both deterministic, with the model sat in between.&lt;/p&gt;
&lt;p&gt;go-tool-base and rust-tool-base converge on the same approach. Snapshot the prompt, with golden files or &lt;code&gt;insta&lt;/code&gt;, so a refactor can&amp;rsquo;t change what you send without a test noticing. Mock the response, with generated &lt;code&gt;ChatClient&lt;/code&gt; mocks or a &lt;code&gt;wiremock&lt;/code&gt; server, so tests run with no network and you can feed in the malformed and error cases a real model won&amp;rsquo;t reliably produce. Leave &amp;ldquo;are the answers any good&amp;rdquo; to a separate evaluation suite. Test the two halves you own, and the non-determinism in the middle stops being an excuse to leave the riskiest line uncovered.&lt;/p&gt;</description></item><item><title>The AI provider that isn't an API</title><link>https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/cover-the-ai-provider-that-isnt-an-api.png" alt="Featured image of post The AI provider that isn't an API" /&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package puts five AI providers behind one interface. Four of them are exactly what you&amp;rsquo;d guess: HTTP calls to OpenAI, Claude, Gemini, and anything OpenAI-compatible. The fifth one isn&amp;rsquo;t an API at all. It shells out to a binary.&lt;/p&gt;
&lt;p&gt;That sounds like a slightly mad thing to want, right up until you&amp;rsquo;ve worked somewhere the network says no.&lt;/p&gt;
&lt;h2 id="the-fifth-provider-shells-out"&gt;The fifth provider shells out
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;chat&lt;/code&gt; package speaks to five providers through one &lt;code&gt;ChatClient&lt;/code&gt; interface. Four of them are what you&amp;rsquo;d expect: HTTP requests to OpenAI, to Claude, to Gemini, to any OpenAI-compatible endpoint. The tool author picks one in config, and the rest of the code never knows the difference.&lt;/p&gt;
&lt;p&gt;The fifth, &lt;code&gt;ProviderClaudeLocal&lt;/code&gt;, is different in kind. It doesn&amp;rsquo;t make an HTTP request at all. It shells out. It runs the &lt;code&gt;claude&lt;/code&gt; CLI binary as a child process, passes the prompt in, and reads the answer back from the binary&amp;rsquo;s output.&lt;/p&gt;
&lt;p&gt;That sounds like an odd thing to want until you&amp;rsquo;ve been stuck in the environment it was built for.&lt;/p&gt;
&lt;h2 id="why-youd-want-that"&gt;Why you&amp;rsquo;d want that
&lt;/h2&gt;&lt;p&gt;Picture a corporate network with its egress locked right down. Outbound HTTPS to &lt;code&gt;api.anthropic.com&lt;/code&gt; is blocked by policy. A tool built on go-tool-base that uses AI would simply fall over there. It tries to reach the API, there&amp;rsquo;s no route, and that&amp;rsquo;s the end of the feature.&lt;/p&gt;
&lt;p&gt;But the developer at that machine has the &lt;code&gt;claude&lt;/code&gt; CLI installed, and has run &lt;code&gt;claude login&lt;/code&gt;. That binary is permitted. It&amp;rsquo;s an approved, managed tool, and it has its own sanctioned path out. The direct API call is blocked; the &lt;code&gt;claude&lt;/code&gt; command is not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is what bridges those two facts. If your tool&amp;rsquo;s AI calls go &lt;em&gt;through&lt;/em&gt; that already-blessed binary instead of straight at the API, they work, in an environment where the direct call cannot. That&amp;rsquo;s the whole reason the provider exists. It isn&amp;rsquo;t faster (a real API call has lower latency) and it isn&amp;rsquo;t more capable. It&amp;rsquo;s for the place where the API call simply isn&amp;rsquo;t an option, and &amp;ldquo;isn&amp;rsquo;t an option&amp;rdquo; is a surprisingly common place to find yourself inside a large organisation.&lt;/p&gt;
&lt;h2 id="what-it-costs-honestly"&gt;What it costs, honestly
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being straight about the trade, because &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is the reduced-capability provider.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t do tool calling. It doesn&amp;rsquo;t do parallel tools. It doesn&amp;rsquo;t stream. Those need a live, structured connection to the model&amp;rsquo;s API, and a subprocess that runs once and prints an answer is not that. What it &lt;em&gt;does&lt;/em&gt; support is plain chat and structured output, the latter through the binary&amp;rsquo;s own &lt;code&gt;--json-schema&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;So the honest positioning, and the package&amp;rsquo;s documentation says exactly this, is: prefer the API providers when you can reach them, because they&amp;rsquo;re lower latency and feature-complete. Reach for &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; when API access is restricted. You accept the narrower capability set as the price of working at all. For a tool whose AI feature is &amp;ldquo;answer a question&amp;rdquo; or &amp;ldquo;return a structured analysis&amp;rdquo;, that price is often nothing you&amp;rsquo;d even notice. For one built on an agentic tool-calling loop, it&amp;rsquo;s a real limitation, and you&amp;rsquo;d know to expect it.&lt;/p&gt;
&lt;h2 id="how-it-stays-behind-the-same-interface"&gt;How it stays behind the same interface
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes it pleasant rather than a special case to maintain. Despite being a subprocess and not an API, &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is still a &lt;code&gt;ChatClient&lt;/code&gt;. Your feature code calls &lt;code&gt;Chat&lt;/code&gt; and &lt;code&gt;Ask&lt;/code&gt; exactly the way it would for any other provider.&lt;/p&gt;
&lt;p&gt;Everything that makes a subprocess provider awkward stays inside the provider. Spawning the binary, feeding it the prompt, parsing its output, capturing &lt;code&gt;stderr&lt;/code&gt; and surfacing it when the binary exits non-zero, and threading multi-turn continuity through session identifiers passed back on the next call with &lt;code&gt;--resume&lt;/code&gt;: all of that is the provider&amp;rsquo;s problem, and all of it sits behind the interface. The code in your tool that uses AI doesn&amp;rsquo;t know, and has no way to find out, that this particular provider is a child process rather than an HTTPS call.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a unified interface genuinely earning its place. It&amp;rsquo;s easy to put a uniform face on four things that already work the same way underneath. The real test of the abstraction is whether something that works in a &lt;em&gt;completely&lt;/em&gt; different way, a subprocess instead of a socket, can still slot in without the caller changing a line. Here it can. You swap one config value, and a tool that talked to an API now talks through a binary, and nothing downstream so much as blinks.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package puts five providers behind one &lt;code&gt;ChatClient&lt;/code&gt; interface, and &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is the one that isn&amp;rsquo;t an API. It runs the locally installed, pre-authenticated &lt;code&gt;claude&lt;/code&gt; CLI as a subprocess.&lt;/p&gt;
&lt;p&gt;It exists for the locked-down environment where outbound HTTPS to the AI API is blocked but the &lt;code&gt;claude&lt;/code&gt; binary is allowed: there, AI features keep working where a direct call would fail. The trade is a narrower capability set (no tool calling, no streaming, plain chat and structured output only) so you prefer the API providers when you can reach them and fall back to this when you can&amp;rsquo;t. And because it&amp;rsquo;s still a &lt;code&gt;ChatClient&lt;/code&gt;, all the subprocess machinery stays hidden, and your code uses it without knowing it&amp;rsquo;s there. That last part is the real test of an abstraction: a provider that works in an entirely different way still slots in unchanged.&lt;/p&gt;</description></item><item><title>AI conversations you can resume</title><link>https://blog-570662.gitlab.io/ai-conversations-you-can-resume/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/ai-conversations-you-can-resume/</guid><description>&lt;img src="https://blog-570662.gitlab.io/ai-conversations-you-can-resume/cover-ai-conversations-you-can-resume.png" alt="Featured image of post AI conversations you can resume" /&gt;&lt;p&gt;An AI conversation is, fundamentally, its own history. The model&amp;rsquo;s next answer depends on everything said so far. And a CLI tool, by its very nature, forgets everything the moment it exits. Put those two facts together and you get the problem: run an AI command, exit, run it again, and you&amp;rsquo;re talking to someone who&amp;rsquo;s never met you.&lt;/p&gt;
&lt;h2 id="a-cli-forgets-everything"&gt;A CLI forgets everything
&lt;/h2&gt;&lt;p&gt;A long-running service keeps its state in memory for as long as it runs. A CLI tool doesn&amp;rsquo;t get that luxury. It starts, does one thing, exits. The next invocation is a brand-new process with no memory of the last one.&lt;/p&gt;
&lt;p&gt;For most commands that&amp;rsquo;s exactly right, and you wouldn&amp;rsquo;t want it any other way. But an AI conversation is a different kind of beast, because a conversation &lt;em&gt;is&lt;/em&gt; its history. The model&amp;rsquo;s next answer depends on everything said so far. Run an AI command, exit, run it again, and you&amp;rsquo;ve started a fresh conversation with someone who&amp;rsquo;s never met you. For an interactive assistant, or any AI workflow that unfolds across several invocations, that&amp;rsquo;s plainly the wrong behaviour. The user expects to pick up where they left off.&lt;/p&gt;
&lt;h2 id="save-and-restore"&gt;Save and restore
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;chat&lt;/code&gt; package handles this through a &lt;code&gt;PersistentChatClient&lt;/code&gt; interface. Like streaming, it&amp;rsquo;s an optional capability discovered with a type assertion, sitting beside &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the four-method core&lt;/a&gt; rather than bloating it. A client that supports persistence also satisfies this interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PersistentChatClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// store the snapshot somewhere&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A snapshot is a serialisable value that captures the conversation. You store it. Next run, you load it, &lt;code&gt;Restore&lt;/code&gt; it onto a fresh client, re-register your tools, and call &lt;code&gt;Chat&lt;/code&gt; again. &amp;ldquo;Where were we?&amp;rdquo; works, because the model is handed back the whole history.&lt;/p&gt;
&lt;h2 id="a-snapshot-is-opinionated-about-what-it-carries"&gt;A snapshot is opinionated about what it carries
&lt;/h2&gt;&lt;p&gt;The interesting part is what a snapshot does and doesn&amp;rsquo;t contain, because that&amp;rsquo;s a series of deliberate decisions.&lt;/p&gt;
&lt;p&gt;It carries the messages, the system prompt, the model name, and tool &lt;em&gt;metadata&lt;/em&gt;: the names, descriptions and parameter schemas of the tools that were registered.&lt;/p&gt;
&lt;p&gt;It does not carry tool &lt;em&gt;handlers&lt;/em&gt;. Handlers are code, not data; you can&amp;rsquo;t serialise a function meaningfully, so after a restore you re-register them with &lt;code&gt;SetTools&lt;/code&gt;. The snapshot remembers that a tool called &lt;code&gt;read_file&lt;/code&gt; existed and what its shape was; it doesn&amp;rsquo;t try to remember the Go function behind it.&lt;/p&gt;
&lt;p&gt;And it does not carry API tokens. This is the one to dwell on. A snapshot is a file. A file gets synced, backed up, copied between machines, attached to a support ticket by a user trying to be helpful. A snapshot that carried the API key would be a credential leak the moment it left the laptop it was made on. So the snapshot never contains a token, at all. On restore, the client picks the credential up again the ordinary way, from &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;the environment or the keychain&lt;/a&gt;. The conversation and the secret are kept in separate places on purpose, and only one of them is ever in the file.&lt;/p&gt;
&lt;h2 id="encrypted-at-rest-if-you-want-it"&gt;Encrypted at rest, if you want it
&lt;/h2&gt;&lt;p&gt;The package ships a &lt;code&gt;FileStore&lt;/code&gt; that writes snapshots as JSON files, with &lt;code&gt;0600&lt;/code&gt; permissions in a &lt;code&gt;0700&lt;/code&gt; directory, and it can encrypt them. Pass &lt;code&gt;WithEncryption&lt;/code&gt; a 32-byte key and snapshots are written with AES-256-GCM.&lt;/p&gt;
&lt;p&gt;That option exists because a conversation can hold sensitive content even when it holds no credential. The log a user pasted in for analysis, the source file they asked the model to review, the internal details tucked into their questions: none of that is an API key, and all of it might be something you&amp;rsquo;d rather not have sitting in plain JSON in a backup somewhere. Encryption at rest covers it.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FileStore&lt;/code&gt; is also careful about the snapshot identifiers it&amp;rsquo;s handed. An ID has to be a canonical UUID, and the resolved file path is checked to lie inside the store directory, so a snapshot ID arriving from an untrusted source (a CLI flag, a request payload) can&amp;rsquo;t be bent into a path-traversal that reads or writes somewhere it shouldn&amp;rsquo;t. Persisting conversations adds a small filesystem surface, and the store treats it as exactly that.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;A CLI tool forgets everything between invocations, which is correct for most commands and wrong for an AI conversation, because a conversation is its history.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package lets you persist one. &lt;code&gt;PersistentChatClient&lt;/code&gt; saves a snapshot you can store and restore later, picking the conversation back up where it ended. The snapshot is deliberate about its contents: messages, system prompt and tool metadata yes; tool handlers no, because they&amp;rsquo;re code you re-register; API tokens never, because a snapshot is a file and a file travels. The built-in &lt;code&gt;FileStore&lt;/code&gt; can encrypt snapshots at rest with AES-256-GCM and validates snapshot IDs against path traversal. Resumable conversations, without the conversation file turning into a place secrets leak from.&lt;/p&gt;</description></item><item><title>An AI agent that has to make the build pass</title><link>https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/</guid><description>&lt;img src="https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/cover-an-ai-agent-that-has-to-make-the-build-pass.png" alt="Featured image of post An AI agent that has to make the build pass" /&gt;&lt;p&gt;Most AI code generation works on a charming little principle I&amp;rsquo;ll call generate-and-hope. The model writes the code, the model stops at the closing brace, and whether the thing actually compiles is left as an exercise for you. For a snippet you paste into an editor, fine. For a whole generated command, that&amp;rsquo;s just outsourcing the disappointment.&lt;/p&gt;
&lt;p&gt;go-tool-base does something I&amp;rsquo;m rather happier with: the AI has to make the build pass before it&amp;rsquo;s allowed to claim it&amp;rsquo;s done.&lt;/p&gt;
&lt;h2 id="generate-and-hope"&gt;Generate and hope
&lt;/h2&gt;&lt;p&gt;The usual shape of AI code generation is this. You ask for code, the model produces it, and the model&amp;rsquo;s job ends at the closing brace. Whether it compiles, whether the tests pass, whether the imports even resolve, none of that has been checked. The model produced something that &lt;em&gt;looks&lt;/em&gt; right. You find out whether it &lt;em&gt;is&lt;/em&gt; right when you build it.&lt;/p&gt;
&lt;p&gt;For a snippet you paste into an editor, that&amp;rsquo;s perfectly fine. The compiler tells you in a second. But go-tool-base&amp;rsquo;s generator, driven by &lt;code&gt;gtb generate command --script&lt;/code&gt; or &lt;code&gt;--prompt&lt;/code&gt;, produces a whole command: the implementation, its tests, the lot. &amp;ldquo;Generate and hope&amp;rdquo; at that scale means handing the user a project that may or may not build, and quietly making them the one who finds out which.&lt;/p&gt;
&lt;h2 id="drafting-is-only-step-one"&gt;Drafting is only step one
&lt;/h2&gt;&lt;p&gt;So the generator doesn&amp;rsquo;t stop at drafting. Writing the first version of the implementation and its tests is step one of two. Step two is an autonomous repair agent.&lt;/p&gt;
&lt;p&gt;Once the draft is on the filesystem, a separate agent takes over. It&amp;rsquo;s an LLM running in a loop, but a loop aimed at one narrow, checkable job: make this project build and pass its tests. It isn&amp;rsquo;t asked to be creative. It&amp;rsquo;s asked to get to green.&lt;/p&gt;
&lt;h2 id="a-fixed-set-of-tools-and-no-shell"&gt;A fixed set of tools, and no shell
&lt;/h2&gt;&lt;p&gt;The agent is not handed a shell. It&amp;rsquo;s given a fixed, defined set of tools and nothing else. Three of them let it explore and edit the project: &lt;code&gt;list_dir&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;. Four of them let it verify the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;go_build&lt;/code&gt; runs the build and captures the compiler errors.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go_test&lt;/code&gt; runs the tests and captures the failures.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go_get&lt;/code&gt; resolves a missing dependency.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;golangci_lint&lt;/code&gt; runs the project&amp;rsquo;s linter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That restriction is the design, not a limitation of it. The agent can&amp;rsquo;t delete arbitrary files, can&amp;rsquo;t reach the network, can&amp;rsquo;t run anything that isn&amp;rsquo;t on the list. It has exactly what it needs to make code compile and nothing it would need to do damage. Its file writes are confined to the project directory by an explicit path check, so even &lt;code&gt;write_file&lt;/code&gt; can&amp;rsquo;t go wandering up into &lt;code&gt;/etc&lt;/code&gt;. A coding agent you&amp;rsquo;d actually let near a filesystem is one whose abilities are an allowlist, not a denylist. (I keep coming back to that principle through this series&amp;hellip; safety as a boundary you draw, not a behaviour you hope for.)&lt;/p&gt;
&lt;h2 id="the-loop"&gt;The loop
&lt;/h2&gt;&lt;p&gt;The repair loop is a ReAct loop, the same reason-act-observe shape as &lt;a class="link" href="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/" &gt;the tool-calling loop&lt;/a&gt;, only this time pointed at a goal:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The draft is on disk.&lt;/li&gt;
&lt;li&gt;Verify: run &lt;code&gt;go_build&lt;/code&gt; and &lt;code&gt;go_test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If verification failed, read the error logs, the compiler error or the failing test.&lt;/li&gt;
&lt;li&gt;Reason about the cause: an undefined variable, a missing import, a wrong signature.&lt;/li&gt;
&lt;li&gt;Act: call &lt;code&gt;write_file&lt;/code&gt; to patch the code, or &lt;code&gt;go_get&lt;/code&gt; to add the dependency.&lt;/li&gt;
&lt;li&gt;Loop. Steps two to five repeat until the project is green, or the agent hits its step limit, which defaults to 15.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What makes this work is treating the error output as &lt;em&gt;feedback&lt;/em&gt; rather than as a failure to log and walk away from. A compiler error is the single most useful sentence you can hand a model that&amp;rsquo;s trying to fix code. It says what&amp;rsquo;s wrong, and usually where. The loop feeds it straight back in, and the model fixes against it.&lt;/p&gt;
&lt;h2 id="verification-changes-what-done-means"&gt;Verification changes what &amp;ldquo;done&amp;rdquo; means
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the real shift, and the agent&amp;rsquo;s own documentation puts it well: the agent &amp;ldquo;doesn&amp;rsquo;t just say it fixed a bug; it uses a Test tool to verify the fix before reporting success.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;A generate-and-hope model reports success when it finishes &lt;em&gt;writing&lt;/em&gt;. It has no idea whether the code works, and it isn&amp;rsquo;t really claiming otherwise. &amp;ldquo;Done&amp;rdquo; means &amp;ldquo;I produced text&amp;rdquo;. The repair agent reports success when &lt;code&gt;go_build&lt;/code&gt; and &lt;code&gt;go_test&lt;/code&gt; actually &lt;em&gt;pass&lt;/em&gt;. &amp;ldquo;Done&amp;rdquo; means &amp;ldquo;the build is green&amp;rdquo;. Those are two completely different claims, and only the second is worth anything to the person who asked for the command.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the line between an AI that&amp;rsquo;s a creative writer and an AI that&amp;rsquo;s a collaborator you can hand a task to. And when the agent can&amp;rsquo;t reach green, when it spends its whole step budget and the project is still broken, the generator fails safely: it leaves the best-attempt code in place, commented out so the project still compiles, and tells the user what to finish by hand. There&amp;rsquo;s also an &lt;code&gt;--agentless&lt;/code&gt; flag for anyone who&amp;rsquo;d rather have a plain single-shot retry than the multi-step agent. The default, though, is the agent, because the default should be code that&amp;rsquo;s been checked.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;Most AI code generation generates and hopes: the model writes code and the user discovers whether it works. For a whole generated command, that pushes a may-or-may-not-build project onto the user.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s generator drafts the command and then hands it to an autonomous repair agent. The agent has a fixed set of tools (explore and edit the project, build it, test it, lint it, fetch dependencies) and no shell at all, with file writes confined to the project directory. It runs a ReAct loop, reading each error and patching against it, until the build is green or it exhausts its steps. The point is what &amp;ldquo;done&amp;rdquo; comes to mean: not &amp;ldquo;the model finished writing&amp;rdquo;, but &amp;ldquo;the build passes&amp;rdquo;. Only one of those is a claim worth trusting.&lt;/p&gt;</description></item><item><title>Stop regex-ing the LLM's prose</title><link>https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/</guid><description>&lt;img src="https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/cover-stop-regexing-the-llms-prose.png" alt="Featured image of post Stop regex-ing the LLM's prose" /&gt;&lt;p&gt;Ask an LLM a question and it hands you back prose. Lovely to read, miserable to program against. You wanted the one number buried in the middle of it, and now you&amp;rsquo;re writing a regular expression to fish a word out of three well-written paragraphs that phrase themselves slightly differently every single time you run them.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a much better way, and it&amp;rsquo;s the difference between forever interpreting an LLM and actually building on one.&lt;/p&gt;
&lt;h2 id="the-problem-with-a-paragraph"&gt;The problem with a paragraph
&lt;/h2&gt;&lt;p&gt;You ask an LLM to analyse a log file and tell you the severity of what it found and a suggested fix. It comes back with three well-written paragraphs. Somewhere in there is the word &amp;ldquo;critical&amp;rdquo;, and somewhere is the fix.&lt;/p&gt;
&lt;p&gt;Your program now has to &lt;em&gt;extract&lt;/em&gt; those two facts from prose, and prose has no contract. The next run, the model phrases it differently. It leads with a caveat. It says &amp;ldquo;severe&amp;rdquo; where last time it said &amp;ldquo;critical&amp;rdquo;. It puts the fix first. Anything that worked by finding &amp;ldquo;critical&amp;rdquo; in the text is now quietly wrong, and you didn&amp;rsquo;t change a line. Parsing free text for structured facts is a game you lose slowly.&lt;/p&gt;
&lt;p&gt;What you actually wanted was never a paragraph. It was a value: a thing with a &lt;code&gt;severity&lt;/code&gt; field and a &lt;code&gt;fix&lt;/code&gt; field, that you can branch on and store and pass around like any other.&lt;/p&gt;
&lt;h2 id="ask-for-the-struct-not-the-prose"&gt;Ask for the struct, not the prose
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package draws the line with two methods. &lt;code&gt;Chat&lt;/code&gt; gives you text. &lt;code&gt;Ask&lt;/code&gt; gives you a struct.&lt;/p&gt;
&lt;p&gt;You define the Go type you want back:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;severity&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;fix&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Analyse this log file: &amp;#34;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;logText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The framework generates a JSON Schema from that struct, sends it to the model as the required response format, and unmarshals the reply straight into &lt;code&gt;result&lt;/code&gt;. You never lay a finger on the prose. You get &lt;code&gt;result.Severity&lt;/code&gt; and &lt;code&gt;result.Fix&lt;/code&gt;, typed, ready to use. If you want the model&amp;rsquo;s answer to drive a &lt;code&gt;switch&lt;/code&gt; statement, this is the method that lets it.&lt;/p&gt;
&lt;h2 id="the-struct-is-the-schema-is-the-contract"&gt;The struct is the schema is the contract
&lt;/h2&gt;&lt;p&gt;The detail that makes this hold up over time: you don&amp;rsquo;t write the schema. The struct &lt;em&gt;is&lt;/em&gt; the schema.&lt;/p&gt;
&lt;p&gt;The framework derives the JSON Schema from your type. In go-tool-base that&amp;rsquo;s &lt;code&gt;GenerateSchema[T]()&lt;/code&gt;; in rust-tool-base the schema comes from your Rust type through &lt;code&gt;schemars&lt;/code&gt;. (Yes, there&amp;rsquo;s a Rust sibling now. I&amp;rsquo;ll &lt;a class="link" href="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/" &gt;introduce it properly&lt;/a&gt; in a few weeks, but it keeps gatecrashing these posts because the two frameworks deliberately share ideas.) Either way there&amp;rsquo;s one definition, your type, and the schema is just a projection of it.&lt;/p&gt;
&lt;p&gt;That matters, because otherwise two things have to agree. There&amp;rsquo;s the schema you tell the model to obey, and there&amp;rsquo;s the type you unmarshal the answer into. Hand-write the schema and those two can drift: add a field to the struct, forget to add it to the schema, and the model is never told to produce it, so it silently never appears. Deriving the schema from the type collapses the two into one. They can&amp;rsquo;t disagree, because there&amp;rsquo;s only one of them.&lt;/p&gt;
&lt;h2 id="both-frameworks-with-one-extra-step-in-rust"&gt;Both frameworks, with one extra step in Rust
&lt;/h2&gt;&lt;p&gt;go-tool-base does this with &lt;code&gt;Ask&lt;/code&gt; and a &lt;code&gt;ResponseSchema&lt;/code&gt; set on the client config. rust-tool-base does it with &lt;code&gt;chat_structured::&amp;lt;T&amp;gt;&lt;/code&gt;, where &lt;code&gt;T&lt;/code&gt; is any type that&amp;rsquo;s both deserialisable and &lt;code&gt;JsonSchema&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base adds one step worth calling out. Before it deserialises the model&amp;rsquo;s reply into your &lt;code&gt;T&lt;/code&gt;, it &lt;em&gt;validates&lt;/em&gt; the raw response against the schema with a JSON Schema validator. That splits the failure into two distinct, named cases: the response didn&amp;rsquo;t match the schema, or it matched the schema but still wouldn&amp;rsquo;t deserialise. A model that returns subtly wrong JSON fails loudly and specifically, with an error that tells you which of those happened, instead of quietly handing you a zero-valued struct that you end up debugging an hour later.&lt;/p&gt;
&lt;h2 id="when-youd-reach-for-it"&gt;When you&amp;rsquo;d reach for it
&lt;/h2&gt;&lt;p&gt;The line is simple, and it&amp;rsquo;s about who reads the answer.&lt;/p&gt;
&lt;p&gt;If a &lt;em&gt;human&lt;/em&gt; reads the answer, prose is right. &lt;code&gt;Chat&lt;/code&gt;, free text, let the model write well. A summary, an explanation, an interactive reply: leave all of those as prose.&lt;/p&gt;
&lt;p&gt;If a &lt;em&gt;program&lt;/em&gt; consumes the answer, you want a value. Classification, extraction, a code review scored out of a hundred with a list of issues, a yes-or-no with reasons: anything where the next thing that happens is your code branching on the result. There, &lt;code&gt;Ask&lt;/code&gt; and &lt;code&gt;chat_structured&lt;/code&gt; turn the LLM from something you have to interpret into something that returns a value, and a typed value is a thing you can actually build on.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;An LLM returns prose by default, and prose has no contract, so a program that picks structured facts out of it breaks the moment the model rephrases.&lt;/p&gt;
&lt;p&gt;Structured output asks for the value instead. You define a struct, the framework derives a JSON Schema from it, the model is constrained to that shape, and you get a typed result. go-tool-base&amp;rsquo;s &lt;code&gt;Ask&lt;/code&gt; and rust-tool-base&amp;rsquo;s &lt;code&gt;chat_structured&lt;/code&gt; both work this way, with the schema derived from your type so the schema and the type can&amp;rsquo;t drift; rust-tool-base additionally validates the response against the schema before deserialising. Use it whenever the answer feeds code rather than a human. It&amp;rsquo;s one of the four methods that make up &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;go-tool-base&amp;rsquo;s small chat interface&lt;/a&gt;, and it&amp;rsquo;s the one that makes an LLM safe to program against.&lt;/p&gt;</description></item><item><title>Telemetry that asks first</title><link>https://blog-570662.gitlab.io/telemetry-that-asks-first/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/telemetry-that-asks-first/</guid><description>&lt;img src="https://blog-570662.gitlab.io/telemetry-that-asks-first/cover-telemetry-that-asks-first.png" alt="Featured image of post Telemetry that asks first" /&gt;&lt;p&gt;Usage telemetry is genuinely useful. Knowing which commands people actually run, where the errors cluster, whether anyone ever touched the feature you spent a fortnight on&amp;hellip; that&amp;rsquo;s the stuff that makes you a better maintainer. Wanting it is completely legitimate.&lt;/p&gt;
&lt;p&gt;The trouble is that the &lt;em&gt;usual&lt;/em&gt; way of getting it, on by default and quietly hoovering up everything, is a small betrayal of the people who installed your tool to get a job done. I wasn&amp;rsquo;t willing to build that, so go-tool-base&amp;rsquo;s telemetry starts from a different question.&lt;/p&gt;
&lt;h2 id="the-data-you-want-and-the-line-you-shouldnt-cross"&gt;The data you want, and the line you shouldn&amp;rsquo;t cross
&lt;/h2&gt;&lt;p&gt;If you maintain a tool, you want to know how it&amp;rsquo;s actually used. Which commands matter and which are dead weight. Where the error rate spikes. Whether anyone touched the feature you spent that fortnight on. That information makes you a better maintainer, and, to say it again, wanting it is completely legitimate.&lt;/p&gt;
&lt;p&gt;The trouble is the standard way of getting it. Telemetry on by default. An opt-out buried three levels down in a settings file nobody reads. And once it&amp;rsquo;s running, it quietly collects far more than it ever admitted to: the arguments people passed, the paths they were working in, an IP address for good measure.&lt;/p&gt;
&lt;p&gt;Every one of those is a small betrayal of someone who installed your tool to get a job done, not to become a data point. And the cost when users notice isn&amp;rsquo;t a slap on the wrist. It&amp;rsquo;s trust, and trust in a developer tool does not grow back quickly. A tool that surprises you once with what it was quietly collecting is a tool you uninstall and warn your colleagues about.&lt;/p&gt;
&lt;p&gt;So go-tool-base&amp;rsquo;s telemetry started from a different question. Not &amp;ldquo;how do we collect the most data&amp;rdquo; but &amp;ldquo;how do we collect &lt;em&gt;useful&lt;/em&gt; data without ever putting the user in a position they didn&amp;rsquo;t choose&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="rule-one-it-is-off-until-you-say-otherwise"&gt;Rule one: it is off until you say otherwise
&lt;/h2&gt;&lt;p&gt;The foundation is the simplest possible rule, and it&amp;rsquo;s absolute. Telemetry is &lt;strong&gt;never enabled by default.&lt;/strong&gt; A freshly installed tool built on go-tool-base sends nothing. Not a heartbeat, not a ping, nothing at all.&lt;/p&gt;
&lt;p&gt;It only starts collecting when the user makes an explicit, visible choice to let it. Three honest doors: they run &lt;code&gt;telemetry enable&lt;/code&gt;, they say yes to a clear prompt during &lt;code&gt;init&lt;/code&gt;, or they set &lt;code&gt;TELEMETRY_ENABLED&lt;/code&gt; themselves. All three are deliberate acts. None of them is a pre-ticked box or a default they have to discover and then undo.&lt;/p&gt;
&lt;p&gt;This is opt-&lt;em&gt;in&lt;/em&gt;, and the distinction from a well-hidden opt-&lt;em&gt;out&lt;/em&gt; is the entire point. Opt-out telemetry treats consent as something to be assumed and grudgingly reversed. Opt-in treats it as something that has to be &lt;em&gt;given&lt;/em&gt;. Only one of those is actually consent.&lt;/p&gt;
&lt;h2 id="rule-two-no-personally-identifiable-information-full-stop"&gt;Rule two: no personally identifiable information, full stop
&lt;/h2&gt;&lt;p&gt;Consent to &amp;ldquo;some telemetry&amp;rdquo; is not consent to &amp;ldquo;any telemetry&amp;rdquo;, so the second rule constrains what can ever be collected, even from a user who&amp;rsquo;s opted in.&lt;/p&gt;
&lt;p&gt;No personally identifiable information. The framework does not record command arguments (they routinely contain paths, hostnames, the occasional secret someone&amp;rsquo;s pasted in). It does not record file contents. It does not record IP addresses.&lt;/p&gt;
&lt;p&gt;It does need &lt;em&gt;some&lt;/em&gt; notion of &amp;ldquo;distinct installations&amp;rdquo; for the numbers to mean anything, so it derives a machine ID from a handful of system signals and runs it through SHA-256. What leaves the machine is a hash. It tells you &amp;ldquo;this is the same install as last week&amp;rdquo; and tells you precisely nothing about whose install it is, and the hash can&amp;rsquo;t be walked backwards into the signals it came from.&lt;/p&gt;
&lt;p&gt;The events themselves are deliberately thin. Which command ran, roughly how long it took, whether it errored. The shape of usage, not a transcript of it.&lt;/p&gt;
&lt;h2 id="rule-three-the-author-picks-the-destination"&gt;Rule three: the author picks the destination
&lt;/h2&gt;&lt;p&gt;Even with consent given and PII excluded, there&amp;rsquo;s a third question: where does the data actually &lt;em&gt;go&lt;/em&gt;? go-tool-base doesn&amp;rsquo;t answer that for you, because it can&amp;rsquo;t. A corporate internal tool, an open-source CLI and an air-gapped utility have completely different right answers.&lt;/p&gt;
&lt;p&gt;So the backend is the tool author&amp;rsquo;s choice. The framework ships several (a noop backend, stdout, a file, plain HTTP, and OpenTelemetry over OTLP) and supports custom ones. The noop backend matters more than it looks: it lets a tool wire up the whole telemetry surface, commands and all, while sending data precisely nowhere. A perfectly reasonable, fully supported configuration.&lt;/p&gt;
&lt;p&gt;Pluggable backends also mean the data never has to touch any infrastructure I run. It goes where the tool&amp;rsquo;s author decides, on their terms. The framework provides the plumbing and stays well out of the destination.&lt;/p&gt;
&lt;h2 id="and-a-way-back-out"&gt;And a way back out
&lt;/h2&gt;&lt;p&gt;One last thing, because it&amp;rsquo;s the part that makes the opt-in real rather than decorative. A user who opted in can opt straight back out, and the package includes a GDPR-aligned deletion path, so &amp;ldquo;stop, and remove what you have&amp;rdquo; is an actual supported request rather than a polite fiction.&lt;/p&gt;
&lt;p&gt;Consent you can&amp;rsquo;t withdraw isn&amp;rsquo;t consent. It&amp;rsquo;s a one-way door with a friendly sign on it. The deletion path is what keeps the front door an actual door.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Telemetry is genuinely useful to a maintainer and genuinely dangerous to the trust of the people running the tool, and the usual implementation (on by default, opt-out buried, collecting everything) spends that trust recklessly. go-tool-base&amp;rsquo;s telemetry holds three lines: never enabled without an explicit user action, never collecting personally identifiable information even once enabled, and always sending data to a destination the tool&amp;rsquo;s author chose, up to and including nowhere. A real deletion path makes the opt-in something you can take back.&lt;/p&gt;
&lt;p&gt;You can have your usage numbers. You just have to ask for them, the way you would for anything else that wasn&amp;rsquo;t yours to begin with.&lt;/p&gt;</description></item><item><title>Letting the AI call your Go functions</title><link>https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/</guid><description>&lt;img src="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/cover-letting-the-ai-call-your-go-functions.png" alt="Featured image of post Letting the AI call your Go functions" /&gt;&lt;p&gt;An AI that can only produce text can &lt;em&gt;describe&lt;/em&gt; your system. An AI that can call your Go functions can actually operate it. That gap, between describing and doing, is the difference between a chatbot and something genuinely useful, and crossing it comes down to one fiddly mechanism: tool-calling, and the loop that drives it.&lt;/p&gt;
&lt;h2 id="talking-about-the-system-versus-operating-it"&gt;Talking about the system versus operating it
&lt;/h2&gt;&lt;p&gt;Wire an AI provider into a CLI command and you get something that can talk. Ask it a question, get a paragraph back. Useful, up to a point.&lt;/p&gt;
&lt;p&gt;But notice the ceiling. An AI that can only generate text can &lt;em&gt;describe&lt;/em&gt; things. It can tell you what it would do. What it can&amp;rsquo;t do is look at the actual current state of your system, or take a real action, because it has no hands. It&amp;rsquo;s reasoning in a vacuum about a world it can&amp;rsquo;t reach out and touch.&lt;/p&gt;
&lt;p&gt;The thing that gives it hands is tool-calling. You hand the AI a set of functions it&amp;rsquo;s allowed to call. Now, mid-conversation, it can decide it needs to &lt;em&gt;read that file&lt;/em&gt; before it can answer, or &lt;em&gt;run that query&lt;/em&gt;, or &lt;em&gt;check that status&lt;/em&gt;, and actually go and do it, and then reason about the real result. The AI stops describing your system and starts operating it.&lt;/p&gt;
&lt;h2 id="the-loop-is-the-hard-part"&gt;The loop is the hard part
&lt;/h2&gt;&lt;p&gt;Tool-calling has a shape, and the shape is a loop. The literature calls it ReAct: Reason, Act, Observe.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The AI &lt;strong&gt;reasons&lt;/strong&gt; about the prompt and decides whether it needs a tool.&lt;/li&gt;
&lt;li&gt;If it does, it &lt;strong&gt;acts&lt;/strong&gt;, asking for a specific tool with specific arguments.&lt;/li&gt;
&lt;li&gt;Your code runs the tool and feeds the result back. The AI &lt;strong&gt;observes&lt;/strong&gt; that result.&lt;/li&gt;
&lt;li&gt;Round again. Reason about the new information, maybe call another tool, maybe several. Keep going until the AI has what it needs and produces a final text answer with no more tool calls.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Conceptually simple. Tedious and error-prone to implement by hand every single time: parsing the model&amp;rsquo;s tool-call requests, dispatching to the right function, marshalling arguments in and results out, feeding observations back in the exact format the provider expects, knowing when to stop, and not looping forever if the model gets itself stuck.&lt;/p&gt;
&lt;p&gt;That orchestration is pure plumbing, and it&amp;rsquo;s identical for every tool and every command. So you can probably guess what&amp;rsquo;s coming: go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package owns it. You don&amp;rsquo;t write the loop. You write the tools.&lt;/p&gt;
&lt;h2 id="defining-a-tool"&gt;Defining a tool
&lt;/h2&gt;&lt;p&gt;A &lt;code&gt;chat.Tool&lt;/code&gt; is four things: a name, a description, a parameter schema, and a handler. The description is what the AI reads to decide &lt;em&gt;whether&lt;/em&gt; to use the tool, so it&amp;rsquo;s worth writing well. The schema describes the arguments, and you don&amp;rsquo;t hand-write it. You write a tagged Go struct and let it generate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ReadFileParams&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;path&amp;#34; jsonschema_description:&amp;#34;Relative path to the file&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The struct is the contract. The framework derives the JSON Schema the AI is given straight from those tags, so the schema and the Go type the handler receives can&amp;rsquo;t drift apart, because they share a single source. The handler is then just an ordinary Go function that takes those parameters and returns a result.&lt;/p&gt;
&lt;p&gt;You register your tools with &lt;code&gt;SetTools&lt;/code&gt;, call &lt;code&gt;Chat&lt;/code&gt;, and that&amp;rsquo;s the whole of your involvement. The framework runs the ReAct loop and &lt;code&gt;Chat&lt;/code&gt; returns the AI&amp;rsquo;s final text answer once the loop settles.&lt;/p&gt;
&lt;h2 id="two-details-that-show-it-was-built-for-real-use"&gt;Two details that show it was built for real use
&lt;/h2&gt;&lt;p&gt;A couple of decisions in the loop tell you it&amp;rsquo;s meant for production, not a demo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool errors don&amp;rsquo;t abort the conversation.&lt;/strong&gt; When a handler returns an error, the framework doesn&amp;rsquo;t crash the loop. It hands the error &lt;em&gt;back to the AI as a string&lt;/em&gt;, as just another observation. That&amp;rsquo;s deliberate, and it&amp;rsquo;s right. A real agent should be able to call a tool, watch it fail, and react: try different arguments, take a different route, or tell the user it couldn&amp;rsquo;t manage it. A loop that aborted on the first tool error would be far more brittle than the model driving it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The loop is bounded.&lt;/strong&gt; There&amp;rsquo;s a &lt;code&gt;MaxSteps&lt;/code&gt; limit, default 20. An AI that gets confused could otherwise call tools forever, and a CLI command that never returns is a worse failure than a wrong answer. The cap guarantees the command terminates. The agent gets room to genuinely work a problem across many steps, but not infinite room to flail about in.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also parallel tool execution: when the model asks for several tools in a single step (three independent file reads, say) the framework runs them concurrently rather than one after another, because there&amp;rsquo;s no reason to make the AI sit and wait out a sequence of things that don&amp;rsquo;t depend on each other.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;A text-only AI can describe your system; an AI that can call your functions can operate it. Bridging that gap means tool-calling, and tool-calling means the ReAct loop (reason, act, observe, repeat) whose orchestration is fiddly, identical every time, and not a problem worth solving twice.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package runs the loop for you. You define &lt;code&gt;chat.Tool&lt;/code&gt; values (name, description, a tagged parameter struct that generates its own schema, a handler), call &lt;code&gt;SetTools&lt;/code&gt; and &lt;code&gt;Chat&lt;/code&gt;, and get the final answer. Tool errors go back to the AI as observations so it can recover, and a &lt;code&gt;MaxSteps&lt;/code&gt; cap guarantees the command always terminates. You write Go functions. The framework turns them into things an agent can reach for.&lt;/p&gt;</description></item><item><title>Nobody reads the manual</title><link>https://blog-570662.gitlab.io/nobody-reads-the-manual/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/nobody-reads-the-manual/</guid><description>&lt;img src="https://blog-570662.gitlab.io/nobody-reads-the-manual/cover-nobody-reads-the-manual.png" alt="Featured image of post Nobody reads the manual" /&gt;&lt;p&gt;Let me describe the actual lifecycle of a user meeting your CLI tool, because it&amp;rsquo;s a bit humbling. They run it. It doesn&amp;rsquo;t quite do what they expected. They run it again with &lt;code&gt;--help&lt;/code&gt;. They get a wall of monospaced flag descriptions, skim it, don&amp;rsquo;t find the thing they wanted, and either give up or go and ask a human who already knows.&lt;/p&gt;
&lt;p&gt;Your documentation might be magnificent. It doesn&amp;rsquo;t matter, because the user never reached it.&lt;/p&gt;
&lt;h2 id="the-manual-loses-on-location-not-quality"&gt;The manual loses on location, not quality
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the lifecycle, and notice exactly where it breaks. The documentation might be excellent. It might answer their precise question in full. It doesn&amp;rsquo;t matter, because it&amp;rsquo;s on a website, in another window, behind a search box, and the user is &lt;em&gt;here&lt;/em&gt;, in the terminal, mid-task. The docs lost not on quality but on &lt;em&gt;location&lt;/em&gt;. They simply weren&amp;rsquo;t where the work was.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s answer starts with a decision about location: the documentation gets embedded into the binary itself. Your &lt;code&gt;docs/&lt;/code&gt; folder ships &lt;em&gt;inside&lt;/em&gt; the tool, the same way its default config does. Wherever the tool is installed, the docs are right there alongside it, no network, no browser. That embedding is what makes everything else possible, and there are two things built on top of it.&lt;/p&gt;
&lt;h2 id="a-browser-in-the-terminal"&gt;A browser, in the terminal
&lt;/h2&gt;&lt;p&gt;The first is the &lt;code&gt;docs&lt;/code&gt; command, and it&amp;rsquo;s not &lt;code&gt;--help&lt;/code&gt; with extra steps. It launches a proper Terminal User Interface, built on Bubble Tea.&lt;/p&gt;
&lt;p&gt;It has a sidebar, structured from the project&amp;rsquo;s own &lt;code&gt;zensical.toml&lt;/code&gt; or &lt;code&gt;mkdocs.yml&lt;/code&gt;, so the docs are a navigable tree rather than one flat scroll. Markdown renders with real formatting through Glamour (colour, tables, lists, headings) instead of collapsing into monospaced soup. There&amp;rsquo;s live search across every page, regex included.&lt;/p&gt;
&lt;p&gt;Compared with &lt;code&gt;man&lt;/code&gt; and &lt;code&gt;--help&lt;/code&gt;, the difference isn&amp;rsquo;t a nicer coat of paint. &lt;code&gt;man&lt;/code&gt; gives you linear scrolling and grep; this gives you a structured tree, rich rendering and real search. It&amp;rsquo;s the documentation experience a modern developer expects, except it followed the tool &lt;em&gt;into&lt;/em&gt; the terminal instead of demanding the user leave it.&lt;/p&gt;
&lt;h2 id="a-documentation-assistant-that-wont-make-things-up"&gt;A documentation assistant that won&amp;rsquo;t make things up
&lt;/h2&gt;&lt;p&gt;The second thing built on the embedded docs is the one I find genuinely transformative: &lt;code&gt;docs ask&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The user doesn&amp;rsquo;t navigate anything. They just ask:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool docs ask &lt;span class="s2"&gt;&amp;#34;how do I point this at a self-hosted server?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and get a direct, specific answer. Under the hood, the framework collates the tool&amp;rsquo;s embedded markdown and hands it to the configured AI provider (Claude, OpenAI, Gemini, Claude Local, any OpenAI-compatible endpoint) as the context for the question.&lt;/p&gt;
&lt;p&gt;Now, &amp;ldquo;an AI answers questions about my tool&amp;rdquo; should immediately make you nervous, and the correct thing to be nervous about is hallucination. An AI that confidently invents a flag that doesn&amp;rsquo;t exist, or describes behaviour the tool simply doesn&amp;rsquo;t have, is worse than no assistant at all, because the user &lt;em&gt;trusts&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;This is where embedding the docs pays off a second time, and it&amp;rsquo;s why I keep stressing that the corpus is &lt;em&gt;closed&lt;/em&gt;. The model is instructed to answer &lt;strong&gt;only&lt;/strong&gt; from the tool&amp;rsquo;s actual documentation, and the context it&amp;rsquo;s handed is exactly that documentation and nothing else. It isn&amp;rsquo;t drawing on a vague memory of similar tools from its training data. It&amp;rsquo;s answering from this tool&amp;rsquo;s real, shipped, version-matched docs. The corpus is small, closed and authoritative, which is the combination that keeps the answers honest. &amp;ldquo;Zero hallucination by design&amp;rdquo; isn&amp;rsquo;t a slogan about the model. It&amp;rsquo;s a property of bounding what the model is allowed to look at, which is the same instinct I &lt;a class="link" href="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/" &gt;leaned on with the &lt;code&gt;mcp&lt;/code&gt; command&lt;/a&gt;: the safety comes from the boundary you drew, not from trusting the AI to behave itself.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a nice second-order effect, too. The answer is always about the version of the tool the user actually has, because the docs were embedded into &lt;em&gt;that build&lt;/em&gt;. No mismatch between a website documenting the latest release and the slightly older binary sitting on the user&amp;rsquo;s machine.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Documentation usually loses to &lt;code&gt;--help&lt;/code&gt; not on quality but on location: it&amp;rsquo;s in a browser, and the user is in the terminal. go-tool-base embeds the docs into the binary and surfaces them two ways: a &lt;code&gt;docs&lt;/code&gt; command that&amp;rsquo;s a real TUI browser with a sidebar, rich markdown and search, and &lt;code&gt;docs ask&lt;/code&gt;, which answers natural-language questions using the embedded docs as context.&lt;/p&gt;
&lt;p&gt;Because that context is the tool&amp;rsquo;s own closed, shipped documentation and the model is told to use nothing else, the assistant stays grounded, and it&amp;rsquo;s always describing the exact version the user is holding. The fix for unread documentation was never to write more of it. It was to put it where the work happens and let it answer back.&lt;/p&gt;</description></item><item><title>BDD where it earns its place, and nowhere else</title><link>https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/</guid><description>&lt;img src="https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/cover-bdd-where-it-earns-its-place.png" alt="Featured image of post BDD where it earns its place, and nowhere else" /&gt;&lt;p&gt;I have a slightly complicated relationship with BDD. I&amp;rsquo;ve watched it turn a tangled test suite into something the whole team could read and reason about, and I&amp;rsquo;ve watched it turn a perfectly good unit test into a paragraph of ceremonial English that nobody benefits from. So when go-tool-base brought in Cucumber-style BDD, the interesting decision wasn&amp;rsquo;t adopting it. It was being ruthless about where &lt;em&gt;not&lt;/em&gt; to.&lt;/p&gt;
&lt;h2 id="two-tests-that-hurt-for-different-reasons"&gt;Two tests that hurt for different reasons
&lt;/h2&gt;&lt;p&gt;Most of go-tool-base&amp;rsquo;s tests are ordinary table-driven Go tests, and they&amp;rsquo;re absolutely fine. A function, a slice of input/expected pairs, a loop. Nobody needs Gherkin to understand a parser test.&lt;/p&gt;
&lt;p&gt;But two areas were genuinely painful, and they were painful in the same way: the &lt;em&gt;test&lt;/em&gt; had become harder to understand than the &lt;em&gt;thing it was testing&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The first was &lt;code&gt;pkg/controls&lt;/code&gt;, the &lt;a class="link" href="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/" &gt;service-lifecycle package&lt;/a&gt;. It runs a small state machine (Unknown, Running, Stopping, Stopped) with signal handling, health monitoring, restart policies and graceful shutdown all woven through it. The integration tests for graceful shutdown had grown to over three hundred lines of imperative goroutine and channel coordination. They worked. But reviewing them was a slog, and a test you can&amp;rsquo;t review with confidence is a test you can&amp;rsquo;t trust when it fails. The behaviour being checked, &amp;ldquo;when a shutdown signal arrives mid-startup, the controller stops cleanly&amp;rdquo;, was a simple sentence buried under a heap of synchronisation scaffolding.&lt;/p&gt;
&lt;p&gt;The second was the CLI itself. &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt; are user &lt;em&gt;workflows&lt;/em&gt;. &amp;ldquo;Given a config file with a custom value, when I run init, then the custom value survives the merge.&amp;rdquo; That&amp;rsquo;s already a Given/When/Then; it just happened to be written out as Go.&lt;/p&gt;
&lt;h2 id="godog-and-the-line-i-drew"&gt;Godog, and the line I drew
&lt;/h2&gt;&lt;p&gt;Godog is the official Go implementation of Cucumber. You write &lt;code&gt;.feature&lt;/code&gt; files in plain Gherkin and bind each step to a Go function. The shutdown scenario stops being three hundred lines of channels and becomes this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gherkin" data-lang="gherkin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;Scenario:&lt;/span&gt;&lt;span class="nf"&gt; graceful shutdown completes within the deadline
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt; Given &lt;/span&gt;&lt;span class="nf"&gt;a controller with two registered services
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;When &lt;/span&gt;&lt;span class="nf"&gt;a shutdown signal is received
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;Then &lt;/span&gt;&lt;span class="nf"&gt;both services stop in registration order
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;And &lt;/span&gt;&lt;span class="nf"&gt;the controller reports a clean shutdown
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The goroutine choreography doesn&amp;rsquo;t vanish, of course. It moves into the step definitions, written once and reused. What changes is that the &lt;em&gt;scenario&lt;/em&gt; is now readable by someone who&amp;rsquo;s never opened the file before, including someone from an ops team who&amp;rsquo;ll never write a line of Go but absolutely has opinions about how shutdown should behave.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part I want to dwell on, because it&amp;rsquo;s the part most BDD adoptions get wrong. The first design decision written down for this work was: &lt;strong&gt;strategic, not universal.&lt;/strong&gt; Use Godog &lt;em&gt;only&lt;/em&gt; where BDD adds clarity. Keep table-driven Go tests as the baseline everywhere else.&lt;/p&gt;
&lt;p&gt;That sounds obvious written down. It is not obvious in practice, because BDD has a gravitational pull. Once a team has feature files, there&amp;rsquo;s a powerful urge to express &lt;em&gt;everything&lt;/em&gt; as feature files, for consistency. And that&amp;rsquo;s how you end up with Gherkin scenarios for a pure function (&lt;code&gt;Given the number 2, When I double it, Then I get 4&lt;/code&gt;) which is pure ceremony. You&amp;rsquo;ve wrapped a one-line table test in a paragraph of English and a step-definition indirection, and made it actively worse.&lt;/p&gt;
&lt;p&gt;The honest test for whether BDD belongs is this: &lt;strong&gt;is this test a narrative, or is it a matrix?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A matrix is the same logic with many input/output pairs. That&amp;rsquo;s a table-driven test, that&amp;rsquo;s most unit tests, and Gherkin actively harms them. A narrative is a sequence of steps where the &lt;em&gt;ordering&lt;/em&gt; and the &lt;em&gt;state between steps&lt;/em&gt; is the thing under test, and that&amp;rsquo;s where Gherkin pays for itself. Lifecycle transitions are narratives. A user running three commands in sequence is a narrative. Doubling a number is not.&lt;/p&gt;
&lt;p&gt;go-tool-base drew that line and stuck to it. Feature files live in &lt;code&gt;features/&lt;/code&gt; at the project root, where a non-Go developer can find and read them. Step definitions live in &lt;code&gt;test/e2e/&lt;/code&gt;, kept well away from the unit tests. And the unit tests stayed exactly what they were, because they were already the right tool.&lt;/p&gt;
&lt;h2 id="made-to-fit-not-bolted-on"&gt;Made to fit, not bolted on
&lt;/h2&gt;&lt;p&gt;A couple of smaller decisions kept the BDD layer from feeling like a foreign object.&lt;/p&gt;
&lt;p&gt;It runs under &lt;code&gt;go test&lt;/code&gt;. There&amp;rsquo;s no separate Cucumber runner to install or remember. A &lt;code&gt;godog.TestSuite&lt;/code&gt; is invoked from an ordinary &lt;code&gt;TestFeatures(t *testing.T)&lt;/code&gt;, so the BDD scenarios run in the same &lt;code&gt;go test ./...&lt;/code&gt; as everything else. CI didn&amp;rsquo;t need a new concept bolted onto it.&lt;/p&gt;
&lt;p&gt;And the CLI end-to-end tests build the &lt;code&gt;gtb&lt;/code&gt; binary &lt;em&gt;once&lt;/em&gt; and reuse it across every scenario. Compiling a binary per scenario would make the suite slow enough that people would quietly start skipping it, and a test suite people skip is just decoration. Build once, test many.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base brought in Godog for BDD, but the decision worth writing about is the restraint. BDD was applied to exactly two things: the service-lifecycle state machine, where a 300-line goroutine tangle became a four-line scenario anyone can review, and CLI workflows, which are Given/When/Then by their very nature. Everywhere else, table-driven Go tests remained the baseline, because wrapping a matrix test in Gherkin makes it worse, not better.&lt;/p&gt;
&lt;p&gt;The useful rule: BDD fits a &lt;em&gt;narrative&lt;/em&gt;, ordered steps with meaningful state in between, and fights a &lt;em&gt;matrix&lt;/em&gt;. Adopt it as a scalpel for the narratives. Resist the pull to turn it into a religion.&lt;/p&gt;</description></item><item><title>An AI interface that fits on one screen</title><link>https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/</guid><description>&lt;img src="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/cover-an-ai-interface-that-fits-on-one-screen.png" alt="Featured image of post An AI interface that fits on one screen" /&gt;&lt;p&gt;The moment you decide a CLI tool should talk to an LLM, there&amp;rsquo;s a strong gravitational pull towards reaching for LangChain, or one of its many relatives. It&amp;rsquo;s the obvious move. It&amp;rsquo;s also, for most CLI work, a bit like hiring a removals firm to carry a single box up the stairs.&lt;/p&gt;
&lt;p&gt;Let me explain why go-tool-base went the other way, and what &amp;ldquo;the other way&amp;rdquo; actually looks like.&lt;/p&gt;
&lt;h2 id="the-instinct-and-why-it-overshoots"&gt;The instinct, and why it overshoots
&lt;/h2&gt;&lt;p&gt;When you add AI to a tool, the instinct is to reach for the big general-purpose framework. LangChain and its relatives are capable, and they exist for a real need: orchestrating complex multi-step AI applications, with retrieval pipelines, memory stores, chains of calls, whole fleets of agents.&lt;/p&gt;
&lt;p&gt;Now look at what a CLI tool actually needs from an LLM. It needs to send a prompt and get text back. Sometimes it wants structured data back instead of prose. Sometimes it wants to let the model call a few of the tool&amp;rsquo;s own functions. That&amp;rsquo;s pretty much the whole list.&lt;/p&gt;
&lt;p&gt;Pulling in a framework built to orchestrate retrieval and agent swarms in order to do &lt;em&gt;that&lt;/em&gt; is a poor trade. You take on a large new vocabulary of concepts, a wide dependency surface, and a great deal of abstraction you&amp;rsquo;ll never touch, all to perform three or four operations. The framework isn&amp;rsquo;t wrong. It&amp;rsquo;s just answering a far bigger question than the one a CLI tool is asking.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-chose-instead"&gt;What go-tool-base chose instead
&lt;/h2&gt;&lt;p&gt;go-tool-base didn&amp;rsquo;t reach for a framework. The decision is on the record in its own design notes: before a single line was written, LangChain Go, go-openai, Vercel&amp;rsquo;s AI SDK and around ten other options were evaluated, and not one of them matched what a CLI framework actually needs. So the &lt;code&gt;chat&lt;/code&gt; package was built deliberately small.&lt;/p&gt;
&lt;p&gt;How small? The entire core &lt;code&gt;ChatClient&lt;/code&gt; interface is four methods:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ChatClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;SetTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Add&lt;/code&gt; appends a message to the conversation. &lt;code&gt;Chat&lt;/code&gt; sends a prompt and returns text. &lt;code&gt;Ask&lt;/code&gt; sends a prompt and returns a &lt;em&gt;typed Go struct&lt;/em&gt;, the model&amp;rsquo;s answer unmarshalled straight into a value you defined. &lt;code&gt;SetTools&lt;/code&gt; hands the model a set of your own functions it&amp;rsquo;s allowed to call. That&amp;rsquo;s the whole surface. Downstream code that uses AI never holds anything larger than this, and never has to know which provider is behind it.&lt;/p&gt;
&lt;p&gt;The package&amp;rsquo;s own documentation has a word for this: right-sized. Large enough to solve genuine provider-abstraction complexity, small enough that the full interface fits on a single screen.&lt;/p&gt;
&lt;h2 id="thin-is-not-the-same-as-does-little"&gt;&amp;ldquo;Thin&amp;rdquo; is not the same as &amp;ldquo;does little&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;This is the part worth being precise about, because &amp;ldquo;four methods&amp;rdquo; can sound like &amp;ldquo;barely does anything&amp;rdquo;, and that&amp;rsquo;s the wrong read entirely.&lt;/p&gt;
&lt;p&gt;Behind those four methods sits genuinely awkward work. Five providers (OpenAI, Claude, Gemini, a locally installed &lt;code&gt;claude&lt;/code&gt; binary, and any OpenAI-compatible endpoint) each with a different wire API, all normalised behind the one interface. A &lt;a class="link" href="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/" &gt;tool-calling loop&lt;/a&gt;. Structured output via JSON Schema, made to behave consistently across providers that each express it differently. Error normalisation. Token chunking.&lt;/p&gt;
&lt;p&gt;The point of a thin abstraction is not that there&amp;rsquo;s little underneath it. It&amp;rsquo;s that the &lt;em&gt;interface&lt;/em&gt; stays small while the &lt;em&gt;implementation&lt;/em&gt; quietly absorbs the complexity. Four methods on the surface; five provider integrations and a tool-calling loop below the waterline. The thinness is a property of what the caller sees, not of what the package does. A reach-for-LangChain decision gets that backwards: it exposes the caller to all the machinery, whether or not the caller will ever need it.&lt;/p&gt;
&lt;h2 id="the-core-stays-small-even-as-features-grow"&gt;The core stays small even as features grow
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a neat detail in how &lt;code&gt;chat&lt;/code&gt; keeps the interface from creeping. The package also supports streaming responses and conversation persistence, both of which are real features with real surface area. Neither of them is in the four-method core.&lt;/p&gt;
&lt;p&gt;Instead they&amp;rsquo;re &lt;em&gt;separate, optional&lt;/em&gt; interfaces. A streaming-capable client also satisfies &lt;code&gt;StreamingChatClient&lt;/code&gt;; a persistable one also satisfies &lt;code&gt;PersistentChatClient&lt;/code&gt;. Code that wants those capabilities does a type assertion to ask for them, and code that doesn&amp;rsquo;t simply never sees them. So the common path stays four methods forever. New capabilities arrive as opt-in interfaces alongside the core, not as new methods bolted onto it. The thing that fits on one screen keeps fitting on one screen.&lt;/p&gt;
&lt;h2 id="extensible-without-forking-testable-without-a-network"&gt;Extensible without forking, testable without a network
&lt;/h2&gt;&lt;p&gt;Two more properties keep the package small without making it limiting.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s extensible. The provider list isn&amp;rsquo;t closed. A &lt;code&gt;RegisterProvider&lt;/code&gt; call lets any package contribute a new provider, and &lt;code&gt;chat.New&lt;/code&gt; will route to it. You add a backend without forking &lt;code&gt;pkg/chat&lt;/code&gt; or sending a patch upstream.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s testable. The package ships generated mocks. A downstream tool&amp;rsquo;s AI features can be tested against a mock &lt;code&gt;ChatClient&lt;/code&gt; returning canned responses, with no network, no API key, and no flakiness. Because the interface is four methods, that mock is trivial to set up and complete by construction. A sprawling framework interface is a sprawling thing to fake; a four-method one is not. (I&amp;rsquo;ll come back to testing AI code properly &lt;a class="link" href="https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/" &gt;in a later post&lt;/a&gt;, because it deserves a whole article of its own.)&lt;/p&gt;
&lt;h2 id="the-right-size"&gt;The right size
&lt;/h2&gt;&lt;p&gt;When a CLI tool needs AI, the instinct is a large framework like LangChain. For orchestrating retrieval pipelines and agent swarms, that&amp;rsquo;s exactly the right tool. For sending a prompt, getting a struct back, and letting the model call a few functions, it&amp;rsquo;s enormous overkill.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package is the deliberate alternative, chosen only after LangChain Go and a dozen others were weighed up and rejected. Its core &lt;code&gt;ChatClient&lt;/code&gt; interface is four methods. Underneath sit five normalised providers, a tool-calling loop, structured output and error handling, but the caller sees four methods and never learns which provider is active. Streaming and persistence are opt-in interfaces beside the core, not additions to it. It extends without forking and tests without a network. Right-sized: the complexity is real, but it lives under the interface rather than in it.&lt;/p&gt;</description></item><item><title>Half your users don't have eyes</title><link>https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/</guid><description>&lt;img src="https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/cover-half-your-users-dont-have-eyes.png" alt="Featured image of post Half your users don't have eyes" /&gt;&lt;p&gt;Run a command in your favourite CLI tool and look at what comes back. Colour. Neatly aligned columns. A friendly little summary sentence. Lovely&amp;hellip; if you happen to be a human with eyes.&lt;/p&gt;
&lt;p&gt;But a good half of any tool&amp;rsquo;s users aren&amp;rsquo;t people at all. They&amp;rsquo;re scripts, CI pipelines, bits of automation. And that pretty output you&amp;rsquo;re so proud of is, to them, actively hostile.&lt;/p&gt;
&lt;h2 id="your-tool-has-two-audiences-and-only-serves-one"&gt;Your tool has two audiences and only serves one
&lt;/h2&gt;&lt;p&gt;I made more or less this same point about AI assistants when I argued that &lt;a class="link" href="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/" &gt;your CLI is already an AI tool&lt;/a&gt;. The machines are users too. Here it isn&amp;rsquo;t an AI doing the calling, it&amp;rsquo;s a humble shell script, but the principle is identical.&lt;/p&gt;
&lt;p&gt;Run a CLI command and look at what comes back. Colour. Aligned columns. A friendly summary sentence. It&amp;rsquo;s designed for a person reading a terminal, and for a person reading a terminal it&amp;rsquo;s great.&lt;/p&gt;
&lt;p&gt;Now picture the other half of your users. A deploy script that needs to know which version is installed. A CI job that runs &lt;code&gt;doctor&lt;/code&gt; and wants to fail the build on one specific check. A bit of automation gluing your tool to three others. None of them have eyes. They have parsers.&lt;/p&gt;
&lt;p&gt;So what do they do with your beautiful human output? They butcher it. They &lt;code&gt;grep&lt;/code&gt; for a keyword, &lt;code&gt;awk&lt;/code&gt; out the third field, &lt;code&gt;sed&lt;/code&gt; off a prefix. It works in the demo. Then someone rewords a status line, or adds a column, or the colour codes shift, and every script downstream breaks at once. Silently, too, because a broken &lt;code&gt;grep&lt;/code&gt; returns nothing rather than an error. You changed a sentence and quietly took out somebody&amp;rsquo;s pipeline without ever knowing.&lt;/p&gt;
&lt;p&gt;The human-readable output was never the contract. It just got &lt;em&gt;used&lt;/em&gt; as one, because it was the only output there was.&lt;/p&gt;
&lt;h2 id="give-the-machines-their-own-channel"&gt;Give the machines their own channel
&lt;/h2&gt;&lt;p&gt;The fix is not to make the human output more parseable. That&amp;rsquo;s a trap. You&amp;rsquo;d be constraining prose meant for people in order to satisfy programs, and end up serving neither of them well. The fix is to give programs their own output format, declared and stable, kept well away from the prose.&lt;/p&gt;
&lt;p&gt;So every command built with go-tool-base gets a &lt;code&gt;--output&lt;/code&gt; flag. Leave it alone and you get the friendly human rendering. Pass &lt;code&gt;--output json&lt;/code&gt; and you get something a parser can actually rely on.&lt;/p&gt;
&lt;p&gt;And not just &lt;em&gt;some&lt;/em&gt; JSON. JSON with a fixed shape.&lt;/p&gt;
&lt;h2 id="one-envelope-every-command"&gt;One envelope, every command
&lt;/h2&gt;&lt;p&gt;The temptation with JSON output is to let each command emit whatever structure happens to suit it. Don&amp;rsquo;t. A consumer scripting against five of your commands then has to learn five shapes, and &amp;ldquo;where&amp;rsquo;s the actual payload?&amp;rdquo; has a different answer every single time.&lt;/p&gt;
&lt;p&gt;go-tool-base wraps every command&amp;rsquo;s JSON in one standard &lt;code&gt;Response&lt;/code&gt; envelope:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deploy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;environment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;production&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1.4.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;replicas&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;status&lt;/code&gt; says how it went. &lt;code&gt;command&lt;/code&gt; says what produced it. &lt;code&gt;data&lt;/code&gt; holds the command-specific payload, and &lt;em&gt;only&lt;/em&gt; the payload. Every built-in command (&lt;code&gt;version&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;) emits exactly this shape. So does every command you write, because &lt;code&gt;pkg/output&lt;/code&gt; hands you the envelope rather than letting you freelance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;deploy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The consumer-side payoff is the whole point. A script can check &lt;code&gt;.status&lt;/code&gt; without ever touching &lt;code&gt;.data&lt;/code&gt;. It can pull &lt;code&gt;.data.version&lt;/code&gt; and know the field is there because it&amp;rsquo;s typed, not scraped. It learns the envelope once, and every command in your tool, and every tool built on the framework, honours it. The contract is explicit, versioned, and the same everywhere, which is precisely what the abused human output never was.&lt;/p&gt;
&lt;h2 id="the-human-output-gets-to-relax"&gt;The human output gets to relax
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a quiet second benefit, and it&amp;rsquo;s my favourite kind: the sort you get for free. Once programs have their own reliable channel, the human output is &lt;em&gt;freed&lt;/em&gt;. It no longer has to stay accidentally parseable. You can reword a status line, add colour, restructure a table, make it genuinely nicer to read, and not break a single script, because no script is reading it any more. They&amp;rsquo;re all over on &lt;code&gt;--output json&lt;/code&gt;, where the real contract lives.&lt;/p&gt;
&lt;p&gt;Two audiences, two formats, each one actually suited to its reader. That&amp;rsquo;s the deal a CLI tool ought to be offering, and most of them don&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;A CLI tool that only emits human-readable output is only half-built, because half its users are programs that end up &lt;code&gt;grep&lt;/code&gt;-ing prose and shattering the moment that prose changes. go-tool-base gives every command a &lt;code&gt;--output json&lt;/code&gt; flag and one standard &lt;code&gt;Response&lt;/code&gt; envelope (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;) used identically by every built-in command and by anything you write through &lt;code&gt;pkg/output&lt;/code&gt;. Machines get a stable, explicit, learn-it-once contract; humans get output that&amp;rsquo;s now free to be properly readable, because nothing fragile depends on its wording any more.&lt;/p&gt;
&lt;p&gt;If your tool will ever be called by another program (and it will), give that program a front door. Don&amp;rsquo;t make it climb in through the window.&lt;/p&gt;</description></item><item><title>Lifecycle management for when your CLI grows up into a service</title><link>https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/</link><pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/</guid><description>&lt;img src="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/cover-lifecycle-management-for-long-running-go-services.png" alt="Featured image of post Lifecycle management for when your CLI grows up into a service" /&gt;&lt;p&gt;There&amp;rsquo;s a moment in the life of a lot of CLI tools where they stop being a CLI tool. Nobody quite decides it. It just happens. Someone needs the thing to also expose a little HTTP endpoint, or poll a queue, or run a scheduler, so it grows a &lt;code&gt;serve&lt;/code&gt; command&amp;hellip; and the honest command-line utility you wrote is suddenly a long-running service wearing a CLI as a hat.&lt;/p&gt;
&lt;p&gt;And a service needs a whole pile of production plumbing that a one-shot command never did.&lt;/p&gt;
&lt;h2 id="the-command-that-stops-being-a-command"&gt;The command that stops being a command
&lt;/h2&gt;&lt;p&gt;go-tool-base is CLI-first. It is not CLI-&lt;em&gt;only&lt;/em&gt;, and the reason is a pattern I&amp;rsquo;ve watched play out more times than I can count.&lt;/p&gt;
&lt;p&gt;A tool starts its life as an honest command-line utility. It runs, it does its thing, it exits. Then someone needs it to expose a small HTTP endpoint. Or poll a queue. Or run a scheduler. So it grows a &lt;code&gt;serve&lt;/code&gt; command, or a &lt;code&gt;run&lt;/code&gt; command, and the moment it does, the thing that was a CLI tool is now a long-running service that happens to have a CLI bolted on the front.&lt;/p&gt;
&lt;p&gt;And a long-running service needs a whole category of plumbing a one-shot command never did. It has to start things up in a sensible order. It has to shut them down &lt;em&gt;gracefully&lt;/em&gt; when someone sends a &lt;code&gt;SIGTERM&lt;/code&gt;, finishing in-flight work rather than dropping it on the floor. It has to tell an orchestrator whether it&amp;rsquo;s alive, and whether it&amp;rsquo;s ready. It has to do something sensible when one of its internal services quietly falls over at 3am.&lt;/p&gt;
&lt;p&gt;Hand-rolled, that&amp;rsquo;s a few hundred lines of goroutine choreography, channel-wrangling and signal handling that every such tool reinvents, slightly differently and slightly wrong each time. It&amp;rsquo;s the first-afternoon problem all over again, just turning up later in the project&amp;rsquo;s life. So go-tool-base ships it: &lt;code&gt;pkg/controls&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-controller-and-the-things-it-controls"&gt;A controller and the things it controls
&lt;/h2&gt;&lt;p&gt;The model is small. A &lt;code&gt;Controller&lt;/code&gt; manages any number of services, each of which satisfies a &lt;code&gt;Controllable&lt;/code&gt; interface, which at heart is just a &lt;code&gt;StartFunc&lt;/code&gt; and a &lt;code&gt;StopFunc&lt;/code&gt;. An HTTP server, a background worker, a scheduler, anything with a &amp;ldquo;begin&amp;rdquo; and an &amp;ldquo;end&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You register your services with the controller and it owns their collective lifecycle. They share a common set of channels (errors, OS signals, health, control messages) so the whole set can react together. A &lt;code&gt;SIGTERM&lt;/code&gt; doesn&amp;rsquo;t get caught by one service off in a corner; it reaches the controller, and the controller takes everything down in order, each &lt;code&gt;StopFunc&lt;/code&gt; handed a context with a deadline so that one sulking service can&amp;rsquo;t wedge the whole shutdown forever.&lt;/p&gt;
&lt;p&gt;That ordering and timeout handling is the bit nobody enjoys writing and everybody needs. Centralising it means a tool that adds a second service later inherits correct coordinated shutdown for free, rather than discovering on its first production &lt;code&gt;SIGTERM&lt;/code&gt; that it only half shuts down.&lt;/p&gt;
&lt;h2 id="probes-because-something-is-usually-watching"&gt;Probes, because something is usually watching
&lt;/h2&gt;&lt;p&gt;If the service ends up in Kubernetes (and a lot of them do) the orchestrator wants to ask two different questions, and they really are different questions.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Liveness:&lt;/em&gt; are you alive, or are you wedged and in need of a kill? &lt;em&gt;Readiness:&lt;/em&gt; are you alive &lt;em&gt;and&lt;/em&gt; able to take traffic right now? A service can quite easily be live but not ready&amp;hellip; still warming a cache, still waiting on a dependency. Conflate the two and you get yourself killed during a slow startup, or sent traffic before you can actually serve it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;controls&lt;/code&gt; keeps them separate. You attach a &lt;code&gt;WithLiveness&lt;/code&gt; probe and a &lt;code&gt;WithReadiness&lt;/code&gt; probe to a service, each just a function returning a health report, and the controller exposes them. The tool answers Kubernetes honestly, in Kubernetes&amp;rsquo; own terms, without you hand-wiring two more HTTP handlers.&lt;/p&gt;
&lt;h2 id="self-healing-but-only-if-you-ask"&gt;Self-healing, but only if you ask
&lt;/h2&gt;&lt;p&gt;The last piece is what happens when a service fails. A worker&amp;rsquo;s &lt;code&gt;StartFunc&lt;/code&gt; returns an error. Health checks start failing. In a hand-rolled setup this is where you either crash the whole process or write yourself a bespoke restart loop.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;controls&lt;/code&gt; has a supervisor that can restart a failed service for you, and the important word in that sentence is &lt;em&gt;can&lt;/em&gt;. It&amp;rsquo;s off by default. A service is only supervised if you hand it a &lt;code&gt;RestartPolicy&lt;/code&gt; at registration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRestartPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RestartPolicy&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MaxRestarts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;InitialBackoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MaxBackoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HealthFailureThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a policy in place, the controller restarts the service if its &lt;code&gt;StartFunc&lt;/code&gt; errors out, or if it racks up more consecutive health-check failures than the threshold allows. Restarts back off exponentially, from &lt;code&gt;InitialBackoff&lt;/code&gt; up to a &lt;code&gt;MaxBackoff&lt;/code&gt; ceiling, so a service that&amp;rsquo;s failing because its database is down doesn&amp;rsquo;t sit there hammering that database flat with a tight restart loop. &lt;code&gt;MaxRestarts&lt;/code&gt; caps the attempts, because a service that&amp;rsquo;s failed five times in a row is not going to be rescued by a sixth go, and at that point honest failure beats a thrashing pretence of health.&lt;/p&gt;
&lt;p&gt;Opt-in matters here. Automatic restarts are exactly right for a resilient daemon and exactly &lt;em&gt;wrong&lt;/em&gt; for a tool where a failure should stop the line and get a human&amp;rsquo;s attention. The framework doesn&amp;rsquo;t make that call for you. It gives you the supervisor and lets you point it at the services that genuinely want it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;A surprising number of CLI tools become long-running services the day they grow a &lt;code&gt;serve&lt;/code&gt; command, and the day they do, they need coordinated startup, graceful ordered shutdown, real liveness and readiness probes, and a considered answer to a service falling over. That&amp;rsquo;s a few hundred lines of fiddly, easy-to-get-wrong plumbing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pkg/controls&lt;/code&gt; provides it: a &lt;code&gt;Controller&lt;/code&gt; over &lt;code&gt;Controllable&lt;/code&gt; services with shared channels and deadline-bounded graceful shutdown, separate Kubernetes-style liveness and readiness probes, and an opt-in supervisor that restarts failed services with exponential backoff and a restart ceiling. Your tool can start as a command and grow into a daemon without that growth turning into a rewrite.&lt;/p&gt;
&lt;p&gt;CLI-first, but not stuck there.&lt;/p&gt;</description></item><item><title>Middleware for CLI commands, not just web servers</title><link>https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/</link><pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/</guid><description>&lt;img src="https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/cover-middleware-for-cli-commands-not-just-web-servers.png" alt="Featured image of post Middleware for CLI commands, not just web servers" /&gt;&lt;p&gt;Every CLI tool past a certain size grows a category of logic that doesn&amp;rsquo;t really belong to any one command, and yet has to happen for loads of them. Timing. An auth check. Panic recovery, so a crash becomes a clean error instead of a stack-trace all over someone&amp;rsquo;s terminal. A log line saying the command started and how it finished.&lt;/p&gt;
&lt;p&gt;Web frameworks sorted this out years ago. CLIs, for some reason, mostly still copy-paste it around.&lt;/p&gt;
&lt;h2 id="the-logic-that-belongs-to-no-single-command"&gt;The logic that belongs to no single command
&lt;/h2&gt;&lt;p&gt;That category of logic doesn&amp;rsquo;t belong to any one command, yet needs to happen for many of them. Time how long the command took. Check the user is authenticated before a command that needs it. Recover from a panic so a crash becomes a clean error rather than a stack-trace vomited across the screen. Log that the command started and how it ended.&lt;/p&gt;
&lt;p&gt;None of that is the command&amp;rsquo;s &lt;em&gt;job&lt;/em&gt;. The &lt;code&gt;deploy&lt;/code&gt; command&amp;rsquo;s job is to deploy. But timing and recovery and auth still have to happen around it, and around &lt;code&gt;build&lt;/code&gt;, and around &lt;code&gt;sync&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Put that logic &lt;em&gt;inside&lt;/em&gt; each command&amp;rsquo;s &lt;code&gt;RunE&lt;/code&gt; and you&amp;rsquo;ve copied the same six lines into thirty functions, which means thirty places to fix when the logging format changes and thirty chances to forget one of them. Cross-cutting concerns copied by hand don&amp;rsquo;t stay consistent. They drift, every time.&lt;/p&gt;
&lt;h2 id="web-frameworks-already-solved-this"&gt;Web frameworks already solved this
&lt;/h2&gt;&lt;p&gt;This is not a new problem. It&amp;rsquo;s about the oldest problem in web frameworks, and they settled on an answer a long time ago: middleware. Gin has it, Echo has it, every HTTP stack you&amp;rsquo;ve ever touched has it. A middleware is a wrapper that sits around a handler, runs its cross-cutting logic, and calls through to the handler in the middle.&lt;/p&gt;
&lt;p&gt;A CLI command is, structurally, just a handler too. So go-tool-base brings the same pattern to the Cobra command tree, with the same functional Chain shape:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A middleware receives the &lt;em&gt;next&lt;/em&gt; handler in the chain and returns a new handler that wraps it. You compose a stack of them, and each command&amp;rsquo;s real &lt;code&gt;RunE&lt;/code&gt; runs in the middle of the onion. Write the timing logic once, as one middleware, and every command in the chain is timed. Change the log format once and all thirty commands change with it, because there was only ever one copy. (The &amp;ldquo;write it once, in a place where everyone inherits it&amp;rdquo; drum again, which I will keep banging until the series runs out.)&lt;/p&gt;
&lt;h2 id="but-cobra-already-has-prerun"&gt;&amp;ldquo;But Cobra already has PreRun&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;It does, and this is the objection worth answering properly, because Cobra ships &lt;code&gt;PersistentPreRun&lt;/code&gt; and &lt;code&gt;PreRun&lt;/code&gt; hooks and they look, at a glance, like they cover this.&lt;/p&gt;
&lt;p&gt;They don&amp;rsquo;t, and the reason is structural. A &lt;code&gt;PreRun&lt;/code&gt; hook is a thing that happens &lt;em&gt;before&lt;/em&gt; the command. That&amp;rsquo;s all it is. It can&amp;rsquo;t run anything &lt;em&gt;after&lt;/em&gt;. It can&amp;rsquo;t wrap the command in a &lt;code&gt;defer&lt;/code&gt;. It can&amp;rsquo;t catch a panic the command throws. It can&amp;rsquo;t measure how long the command took, because measuring a duration needs a start point &lt;em&gt;and&lt;/em&gt; an end point, and the hook only owns the start.&lt;/p&gt;
&lt;p&gt;A middleware wraps the &lt;em&gt;entire&lt;/em&gt; execution. Because it&amp;rsquo;s a function that calls &lt;code&gt;next()&lt;/code&gt; in its own body, it straddles the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TimingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HandlerFunc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// the command runs here&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;command finished&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;took&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before, after, and around. A recovery middleware can put a &lt;code&gt;defer recover()&lt;/code&gt; in place that a &lt;code&gt;PreRun&lt;/code&gt; hook structurally cannot. An auth middleware can check a condition and return an error &lt;em&gt;instead of calling &lt;code&gt;next()&lt;/code&gt; at all&lt;/em&gt;, refusing to let the command run in the first place. &lt;code&gt;PreRun&lt;/code&gt; can&amp;rsquo;t veto the command; it runs, and then the command runs regardless.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PreRun&lt;/code&gt; is a notification that the command is about to happen. Middleware is control over whether and how it happens. For genuine cross-cutting concerns you need the second thing, not the first.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;Timing, auth, recovery and logging are cross-cutting concerns: necessary for many commands, owned by none. Hand-copied into every &lt;code&gt;RunE&lt;/code&gt;, they drift out of sync. Web frameworks fixed this with middleware years ago, and a CLI command is structurally just another handler.&lt;/p&gt;
&lt;p&gt;go-tool-base brings the functional Chain middleware pattern to the Cobra command tree. A middleware wraps a command&amp;rsquo;s whole execution, so it acts before and after and can decide whether the command runs at all&amp;hellip; strictly more than Cobra&amp;rsquo;s &lt;code&gt;PreRun&lt;/code&gt; hooks, which only fire beforehand and can&amp;rsquo;t wrap, recover, time, or veto. Write the concern once, wrap the chain, and every command inherits it consistently.&lt;/p&gt;</description></item><item><title>A logging interface that doesn't leak its backend</title><link>https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/cover-a-logging-interface-that-doesnt-leak-its-backend.png" alt="Featured image of post A logging interface that doesn't leak its backend" /&gt;&lt;p&gt;The same tool, in two different lives, wants two completely different kinds of log.&lt;/p&gt;
&lt;p&gt;On my laptop I want logs I can actually read: colour, alignment, friendly timestamps. The very same tool running as a daemon in a container wants none of that. It wants structured JSON, one object a line, ready for a log aggregator to swallow. And in a test I want the logger to shut up entirely. The interesting question is what it costs you to move between the three.&lt;/p&gt;
&lt;h2 id="the-same-tool-wants-different-logs"&gt;The same tool wants different logs
&lt;/h2&gt;&lt;p&gt;On a developer&amp;rsquo;s machine the tool is a CLI. You want logs that are pleasant to read in a terminal: colour, alignment, human-friendly timestamps. The charmbracelet logger does that beautifully.&lt;/p&gt;
&lt;p&gt;Then the very same tool grows a &lt;code&gt;serve&lt;/code&gt; command and gets deployed as a daemon in a container. Now coloured terminal output is worse than useless. The log aggregator wants structured JSON, one object per line, machine-parseable. &lt;code&gt;slog&lt;/code&gt; does that.&lt;/p&gt;
&lt;p&gt;And in tests you want neither. You want the logger to exist, satisfy the interface, and stay completely silent.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s three different logging backends, wanted by one tool across three different lives. So what does switching between them actually cost?&lt;/p&gt;
&lt;h2 id="what-it-costs-depends-on-what-your-packages-imported"&gt;What it costs depends on what your packages imported
&lt;/h2&gt;&lt;p&gt;If your packages import a concrete logger, if &lt;code&gt;pkg/config&lt;/code&gt; and &lt;code&gt;pkg/setup&lt;/code&gt; and twenty others each have &lt;code&gt;import &amp;quot;github.com/charmbracelet/log&amp;quot;&lt;/code&gt; and take a &lt;code&gt;*log.Logger&lt;/code&gt;, then the backend is welded into the entire codebase. Switching to JSON for the container build means editing the import and the parameter type in every single one of those packages. The backend has &lt;em&gt;leaked&lt;/em&gt;. A detail that should have been one decision has become a property of a hundred files.&lt;/p&gt;
&lt;p&gt;go-tool-base doesn&amp;rsquo;t let it leak. Every package in the framework accepts a &lt;code&gt;logger.Logger&lt;/code&gt;, an interface, and nothing else. No package anywhere imports a concrete logging library. A package states, in its types, &amp;ldquo;I need something I can log through&amp;rdquo;, and stops right there. It has no idea, and no way to find out, what&amp;rsquo;s actually on the other end.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// what every package depends on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The backend gets chosen once, at the top, when the tool builds its &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt;. It travels down to every package as the interface, through the &lt;code&gt;Props&lt;/code&gt; container. The packages underneath never see the concrete type, so the concrete type can change without a single one of them noticing. (There&amp;rsquo;s that &amp;ldquo;decide it once, in one place&amp;rdquo; theme again. I did warn you it runs through everything.)&lt;/p&gt;
&lt;h2 id="three-backends-and-the-swap-is-one-line"&gt;Three backends, and the swap is one line
&lt;/h2&gt;&lt;p&gt;go-tool-base ships three implementations of that interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;charmbracelet&lt;/strong&gt; (&lt;code&gt;logger.NewCharm(w, opts...)&lt;/code&gt;). Coloured, styled, for humans at a terminal. The CLI default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;slog JSON&lt;/strong&gt;, a &lt;code&gt;slog&lt;/code&gt;-backed backend emitting structured JSON, for daemons and containers feeding a log aggregator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;noop&lt;/strong&gt;, which does precisely nothing, for tests that want a real &lt;code&gt;Logger&lt;/code&gt; and total silence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Switching the tool from a friendly CLI logger to container-ready JSON is a change to the one line in &lt;code&gt;main()&lt;/code&gt; that constructs the logger. That&amp;rsquo;s the lot. &lt;code&gt;pkg/config&lt;/code&gt; doesn&amp;rsquo;t change. &lt;code&gt;pkg/setup&lt;/code&gt; doesn&amp;rsquo;t change. None of the twenty packages change, because none of them ever knew which backend they had. The decision was always one line; the interface is what &lt;em&gt;kept&lt;/em&gt; it one line.&lt;/p&gt;
&lt;p&gt;The noop backend deserves its own mention, because it&amp;rsquo;s the one people underrate. A test for a command shouldn&amp;rsquo;t be spraying log output all over the test run, but the command still needs a non-nil &lt;code&gt;Logger&lt;/code&gt; to function. &lt;code&gt;logger.NewNoop()&lt;/code&gt; gives you exactly that: interface satisfied, output binned, test quiet. And because it&amp;rsquo;s just another implementation of the same interface, no test needs any special logging machinery. It passes a different backend, exactly the way the container build does.&lt;/p&gt;
&lt;h2 id="the-general-shape"&gt;The general shape
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s nothing exotic going on here. It&amp;rsquo;s &amp;ldquo;depend on interfaces, not implementations&amp;rdquo;, which every Go developer has had drilled into them at some point. The bit worth holding onto is &lt;em&gt;where&lt;/em&gt; the rule actually pays out, and it&amp;rsquo;s at the seams between a stable core and a detail you know full well you&amp;rsquo;ll want to vary.&lt;/p&gt;
&lt;p&gt;A logging backend is exactly such a detail. You will want it different in a terminal, in a container, and in a test. So the thing your code depends on has to be the interface, and the concrete backend has to be chosen at one well-known point and nowhere else. Get that boundary right and &amp;ldquo;we need JSON logs in production&amp;rdquo; is a one-line change. Get it wrong and it&amp;rsquo;s a refactor and a bad afternoon.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;One tool legitimately wants three different logging backends across its life: coloured output in a terminal, structured JSON in a container, silence in a test. The cost of moving between them is decided entirely by whether your packages imported a concrete logger or an interface.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s packages depend only on &lt;code&gt;logger.Logger&lt;/code&gt;, never a backend. Three implementations ship (charmbracelet, &lt;code&gt;slog&lt;/code&gt; JSON, noop) and the backend is chosen once, in &lt;code&gt;main()&lt;/code&gt;, then carried everywhere as the interface through &lt;code&gt;Props&lt;/code&gt;. Switching is one line at the top, because the detail was never allowed to leak into the hundred files below it.&lt;/p&gt;</description></item><item><title>Errors that tell the user what to do next</title><link>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/cover-errors-that-tell-the-user-what-to-do-next.png" alt="Featured image of post Errors that tell the user what to do next" /&gt;&lt;p&gt;Here&amp;rsquo;s an error message I&amp;rsquo;ve been on the receiving end of more times than I&amp;rsquo;d care to count:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: failed to read config file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;True. Also completely useless! I now know something is broken and I haven&amp;rsquo;t the faintest idea what to do about it. Which file? Why couldn&amp;rsquo;t it be read? Should I create it, run some &lt;code&gt;init&lt;/code&gt; command, fix a permission, set an environment variable? The message states the problem and then abandons me at it, rather like a sat-nav cheerfully announcing &amp;ldquo;you have arrived&amp;rdquo; in the middle of a motorway.&lt;/p&gt;
&lt;h2 id="a-message-is-not-a-fix"&gt;A message is not a fix
&lt;/h2&gt;&lt;p&gt;The instinct, the moment you notice this, is to go and write a &lt;em&gt;better message&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mytool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Better for the human, no question. But look at what you&amp;rsquo;ve just done to the error as a &lt;em&gt;value&lt;/em&gt;. The recovery advice is now welded into the error string. Any code that wants to ask &amp;ldquo;is this the config-missing error?&amp;rdquo; is reduced to substring-matching English prose. Reword the advice and you break the check. So you&amp;rsquo;ve helped the user and quietly sabotaged the program at the same time, because you&amp;rsquo;ve made one poor little string do two completely incompatible jobs&amp;hellip; being a stable identity for code, and being friendly guidance for people.&lt;/p&gt;
&lt;h2 id="why-i-changed-error-libraries"&gt;Why I changed error libraries
&lt;/h2&gt;&lt;p&gt;go-tool-base started out on &lt;code&gt;github.com/go-errors/errors&lt;/code&gt;. It&amp;rsquo;s a perfectly fine library and it gave us stack traces. What it didn&amp;rsquo;t give us was any way to attach human guidance to an error without shoving it into the message string. So the codebase did exactly the daft thing I just described: multi-line suggestion text baked straight into &lt;code&gt;errors.Errorf&lt;/code&gt; calls, user-facing content and programmatic identity all mashed into one value.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the whole reason for the migration to &lt;code&gt;github.com/cockroachdb/errors&lt;/code&gt;. Not novelty, and not because I fancied a weekend of find-and-replace. One specific capability: &lt;code&gt;cockroachdb/errors&lt;/code&gt; lets you attach a &lt;strong&gt;hint&lt;/strong&gt; to an error as a separate, structured field.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;failed to read config file&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Run &amp;#39;mytool init&amp;#39; to create one, or set MYTOOL_CONFIG to point at an existing file.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now there are two things, cleanly apart. &lt;code&gt;errors.New(&amp;quot;failed to read config file&amp;quot;)&lt;/code&gt; is the &lt;em&gt;identity&lt;/em&gt;&amp;hellip; stable, matchable, the program&amp;rsquo;s handle on the error. The hint is the &lt;em&gt;guidance&lt;/em&gt;&amp;hellip; for the human, and rewordable as much as you like without breaking a single check, because no check ever looks at it. &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt; work properly through every wrapper layer, so code matches on identity and never has to read prose.&lt;/p&gt;
&lt;p&gt;The migration brought a few other things worth having. Stack traces print with a plain &lt;code&gt;%+v&lt;/code&gt; instead of a type assertion. Errors can carry structured, machine-readable metadata. Multiple errors from concurrent work can be combined as a first-class value. But the hint is the one that actually changed the user&amp;rsquo;s day, because the hint is the recovery step, stored where it belongs.&lt;/p&gt;
&lt;h2 id="one-door-out-and-it-knows-where-the-help-is"&gt;One door out, and it knows where the help is
&lt;/h2&gt;&lt;p&gt;Separating the hint is only half of it. The other half is making sure those hints actually reach the user, every time, and that comes down to having a single way out.&lt;/p&gt;
&lt;p&gt;Every go-tool-base command returns its errors the idiomatic Cobra way, through &lt;code&gt;RunE&lt;/code&gt;. They all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error (runtime failure, flag parse error, pre-run failure) through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. So error &lt;em&gt;presentation&lt;/em&gt; gets decided in exactly one place, and no command can render an error differently from the command sat next to it.&lt;/p&gt;
&lt;p&gt;And because there&amp;rsquo;s one handler, it can pull off something the individual commands never could. The framework knows your tool&amp;rsquo;s metadata, including its configured support channel, be it a Slack workspace or a Teams channel. So the error handler can finish a fatal error not just with the &lt;em&gt;what&lt;/em&gt; and the recovery hint, but with &lt;em&gt;where to go if the hint didn&amp;rsquo;t help&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Still&lt;/span&gt; &lt;span class="n"&gt;stuck&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="c1"&gt;#mytool-support on Slack.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The user is never left at a dead end. The error tells them what broke, the hint tells them the most likely fix, and if that&amp;rsquo;s still not enough the handler tells them which door to go and knock on. A failure becomes a signpost instead of a full stop.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;An error that only reports what went wrong leaves the user stranded, and the obvious fix (writing the recovery advice into the message) quietly wrecks the error as a value, because now your code has to substring-match prose just to work out what it&amp;rsquo;s looking at.&lt;/p&gt;
&lt;p&gt;go-tool-base moved from &lt;code&gt;go-errors&lt;/code&gt; to &lt;code&gt;cockroachdb/errors&lt;/code&gt; to get hints: a structured, separate field for human guidance that leaves the error&amp;rsquo;s identity clean for &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt;. Every command&amp;rsquo;s errors leave through one &lt;code&gt;Execute()&lt;/code&gt; wrapper and one &lt;code&gt;ErrorHandler&lt;/code&gt;, so presentation stays consistent, and because that handler knows the tool&amp;rsquo;s support channel it can point a stuck user at real help.&lt;/p&gt;
&lt;p&gt;State the problem for the program. Give the fix to the human. And for pity&amp;rsquo;s sake, keep the two in different fields.&lt;/p&gt;</description></item><item><title>Many embedded filesystems, one merged view</title><link>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</guid><description>&lt;img src="https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/cover-many-embedded-filesystems-one-merged-view.png" alt="Featured image of post Many embedded filesystems, one merged view" /&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;embed&lt;/code&gt; package is one of those features that makes you slightly giddy the first time you use it. One &lt;code&gt;//go:embed&lt;/code&gt; directive and your default config, your templates, your docs are all baked into the binary. The tool just works the moment it&amp;rsquo;s installed, with nothing external to lose or forget to ship.&lt;/p&gt;
&lt;p&gt;And then you go and build something modular on top of it, and you discover the catch nobody warned you about.&lt;/p&gt;
&lt;h2 id="embedfs-is-an-island"&gt;&lt;code&gt;embed.FS&lt;/code&gt; is an island
&lt;/h2&gt;&lt;p&gt;An &lt;code&gt;embed.FS&lt;/code&gt; has a property that&amp;rsquo;s easy to miss until it bites: it&amp;rsquo;s local to the package that declared it. The &lt;code&gt;//go:embed&lt;/code&gt; directive can only see files at or below its own source file. So in any project bigger than a toy, you don&amp;rsquo;t have &lt;em&gt;an&lt;/em&gt; embedded filesystem. You have many. The root package embeds one. Each feature, each subcommand that ships its own templates or defaults, embeds another. They&amp;rsquo;re islands, one per package, and Go gives you no native way to make them behave as a whole.&lt;/p&gt;
&lt;p&gt;For most files that&amp;rsquo;s perfectly fine. A feature&amp;rsquo;s templates can stay on the feature&amp;rsquo;s island; nothing else needs them.&lt;/p&gt;
&lt;p&gt;It stops being fine the moment features need to contribute to something shared.&lt;/p&gt;
&lt;h2 id="the-shared-config-problem"&gt;The shared-config problem
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the case that forces the issue. A go-tool-base tool has a global &lt;code&gt;config.yaml&lt;/code&gt; of defaults, embedded at the root. Now you add a feature, and that feature has its own configuration keys, with their own sensible defaults.&lt;/p&gt;
&lt;p&gt;Where do those defaults go?&lt;/p&gt;
&lt;p&gt;The naive answer is: edit the root &lt;code&gt;config.yaml&lt;/code&gt; and add the feature&amp;rsquo;s section. And that&amp;rsquo;s a genuinely bad answer, because it inverts the dependency. The root config now has to know about every feature. Add a feature, edit the centre. Remove one, edit the centre again. The central file becomes a pinch point that every feature has to reach into, and a modular architecture where you can&amp;rsquo;t add a module without editing the core isn&amp;rsquo;t really modular at all&amp;hellip; it just has more files.&lt;/p&gt;
&lt;p&gt;What you actually want is for the feature to ship its own slice of default config, on its own island, and for the global config the tool reads to somehow already contain it. The feature contributes; the centre doesn&amp;rsquo;t budge.&lt;/p&gt;
&lt;h2 id="propsassets-merge-the-islands"&gt;&lt;code&gt;props.Assets&lt;/code&gt;: merge the islands
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the job of &lt;code&gt;props.Assets&lt;/code&gt;. (Yes, it lives on &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt;, the load-bearing container I keep going on about. Most of the good stuff does.) It&amp;rsquo;s a layer that implements the standard &lt;code&gt;fs.FS&lt;/code&gt; interface, and into it you &lt;code&gt;Register&lt;/code&gt; each &lt;code&gt;embed.FS&lt;/code&gt; under a name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// root main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AssetMap&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// a feature&amp;#39;s command constructor&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:embed assets/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;feature&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now &lt;code&gt;Props&lt;/code&gt; carries one &lt;code&gt;Assets&lt;/code&gt; value that represents all the islands as a single filesystem. The root&amp;rsquo;s files and every registered feature&amp;rsquo;s files, addressable through one &lt;code&gt;fs.FS&lt;/code&gt;. Each registration is named, so the islands stay individually identifiable, but they read as one.&lt;/p&gt;
&lt;p&gt;That alone solves the addressing problem. The genuinely clever part is what happens for structured files.&lt;/p&gt;
&lt;h2 id="opening-a-file-that-exists-in-several-places"&gt;Opening a file that exists in several places
&lt;/h2&gt;&lt;p&gt;When you &lt;code&gt;Open&lt;/code&gt; a path through &lt;code&gt;props.Assets&lt;/code&gt; and that path has a structured extension (&lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;) it doesn&amp;rsquo;t simply return the first match it stumbles across. It does this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; It finds every instance of that path, across every registered filesystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parsing.&lt;/strong&gt; It unmarshals each one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merging.&lt;/strong&gt; It deep-merges the parsed data, using &lt;code&gt;mergo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-serialisation.&lt;/strong&gt; It hands you back a single &lt;code&gt;fs.File&lt;/code&gt; whose contents are the combined, merged result.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So picture the shared-config problem again, only solved this time. The root ships a &lt;code&gt;config.yaml&lt;/code&gt; with the base defaults. Each feature ships a &lt;code&gt;config.yaml&lt;/code&gt; on its own island carrying only its own keys. Nobody edits anybody else&amp;rsquo;s file. When the &lt;code&gt;init&lt;/code&gt; command opens &lt;code&gt;config.yaml&lt;/code&gt; through &lt;code&gt;props.Assets&lt;/code&gt;, it doesn&amp;rsquo;t get the root&amp;rsquo;s copy. It gets the deep-merge of the root&amp;rsquo;s copy and every registered feature&amp;rsquo;s copy: one &lt;code&gt;config.yaml&lt;/code&gt; that contains every default in the tool, assembled at runtime from contributions that never knew about each other.&lt;/p&gt;
&lt;p&gt;A feature contributes its defaults simply by existing and registering. The centre never changes. That&amp;rsquo;s the modular property the naive approach couldn&amp;rsquo;t give you, and it generalises well beyond config&amp;hellip; the same merge applies to a shared &lt;code&gt;commands.csv&lt;/code&gt;, or any structured file features want to add rows or keys to.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;code&gt;Mount&lt;/code&gt; method for attaching an arbitrary &lt;code&gt;fs.FS&lt;/code&gt; at a virtual path, which is handy for surfacing something external (a temp directory, say) as part of the same tree. But the structured merge is the feature that really earns &lt;code&gt;Assets&lt;/code&gt; its place.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;embed.FS&lt;/code&gt; is per-package by design, so a modular CLI ends up with many embedded filesystems, one island per feature. Most of the time that&amp;rsquo;s fine. It fails specifically when features need to contribute to a shared resource like the global &lt;code&gt;config.yaml&lt;/code&gt;, because the naive fix forces every feature to reach in and edit a central file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;props.Assets&lt;/code&gt; merges all the registered islands into a single &lt;code&gt;fs.FS&lt;/code&gt;, and for structured files it goes further: opening a &lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt; or &lt;code&gt;.csv&lt;/code&gt; discovers every copy across every island, deep-merges them, and returns the combined whole. A feature drops its own defaults onto its own island, registers, and the merged config the tool reads already includes them. Contribution without coupling, which is rather the whole point of being modular in the first place.&lt;/p&gt;</description></item><item><title>Props: the container that does the heavy lifting</title><link>https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/</guid><description>&lt;img src="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/cover-props.png" alt="Featured image of post Props: the container that does the heavy lifting" /&gt;&lt;p&gt;I name-dropped &lt;code&gt;Props&lt;/code&gt; back in the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduction&lt;/a&gt; and then rather glossed over it, which was a bit unfair of me, because it&amp;rsquo;s the single most important design decision in the whole framework. So let&amp;rsquo;s give it the attention it actually deserves.&lt;/p&gt;
&lt;p&gt;And the best place to start, oddly enough, is the name.&lt;/p&gt;
&lt;h2 id="start-with-the-name"&gt;Start with the name
&lt;/h2&gt;&lt;p&gt;The container at the centre of go-tool-base is called &lt;code&gt;Props&lt;/code&gt;, and the name is doing real work, so we&amp;rsquo;ll start there.&lt;/p&gt;
&lt;p&gt;It is not short for &amp;ldquo;properties&amp;rdquo;, though it does hold a few. A &lt;em&gt;prop&lt;/em&gt; is the heavy timber or steel beam that stops a structure quietly collapsing in on itself. And for anyone who follows the rugby: a prop is the position in the scrum, the broad-shouldered forward whose entire job is to provide structural support so everyone else can get on with the game.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the design brief, in a single word. &lt;code&gt;Props&lt;/code&gt; is not where the clever, flashy work happens. It scores no tries. It&amp;rsquo;s the unglamorous, load-bearing thing that holds the framework up so that your actual command logic gets to be the interesting part. Understand the name and you understand what the struct is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="what-it-carries"&gt;What it carries
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Props&lt;/code&gt; is the single object passed to every command constructor in a go-tool-base tool. It holds the dependencies a command might need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Tool&lt;/code&gt;, metadata about the CLI (name, summary, release source).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Logger&lt;/code&gt;, the logging abstraction.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Config&lt;/code&gt;, the loaded configuration container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FS&lt;/code&gt;, a filesystem abstraction (&lt;code&gt;afero&lt;/code&gt;), so a command never touches the real disk directly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Assets&lt;/code&gt;, the embedded-resource manager.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Version&lt;/code&gt;, build information.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ErrorHandler&lt;/code&gt;, the centralised error reporter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A command constructor&amp;rsquo;s signature is, accordingly, boring on purpose:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One parameter. Everything the command could possibly need is reachable through it. No globals, no &lt;code&gt;init()&lt;/code&gt;-time wiring, no twelve-argument constructor that quietly grows a thirteenth argument next month.&lt;/p&gt;
&lt;h2 id="why-a-struct-and-not-contextcontext"&gt;Why a struct, and not &lt;code&gt;context.Context&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the design decision I actually want to defend, because it&amp;rsquo;s the one Go developers tend to raise an eyebrow at. Go already has a well-known way to carry things through a call tree: &lt;code&gt;context.Context&lt;/code&gt;. So why not just put the logger and the config in the context and pass that around?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;context.Context&lt;/code&gt; carries its values as &lt;code&gt;interface{}&lt;/code&gt;, and that&amp;rsquo;s the wrong trade for &lt;em&gt;dependencies&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Pull a dependency out of a context and you get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;logger&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// a runtime type assertion&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That one line has two separate ways to hurt you. The key is a bare string, so a typo compiles perfectly happily and then fails at runtime. The type assertion is unchecked, so if the wrong thing is sitting under that key, your tool panics in front of a user. Neither failure is visible to the compiler. Neither is visible to your IDE. You find out when it breaks, which is to say at the worst possible time.&lt;/p&gt;
&lt;p&gt;Pull the same dependency out of &lt;code&gt;Props&lt;/code&gt; and you get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;starting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// a field access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;p.Logger&lt;/code&gt; is a typed field. If it doesn&amp;rsquo;t exist, or you&amp;rsquo;ve used it wrong, the code simply doesn&amp;rsquo;t compile. Your IDE autocompletes it. Refactor the &lt;code&gt;Logger&lt;/code&gt; interface and every misuse lights up at build time. There&amp;rsquo;s no runtime type assertion, because there&amp;rsquo;s no &lt;code&gt;interface{}&lt;/code&gt; to assert from in the first place.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;context.Context&lt;/code&gt; is the right tool for what it was designed for: cancellation, deadlines, request-scoped signals that genuinely cross API boundaries. It&amp;rsquo;s the wrong tool for &amp;ldquo;here are my program&amp;rsquo;s services&amp;rdquo;, because it trades away the compiler&amp;rsquo;s help for a flexibility you really don&amp;rsquo;t want here. Dependencies should be &lt;em&gt;declared&lt;/em&gt;, somewhere the compiler checks them. &lt;code&gt;Props&lt;/code&gt; is that somewhere.&lt;/p&gt;
&lt;h2 id="what-you-get-back-for-it"&gt;What you get back for it
&lt;/h2&gt;&lt;p&gt;That one decision pays out in three currencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testability.&lt;/strong&gt; A command is now a pure function of its &lt;code&gt;Props&lt;/code&gt;. To test it, you build a &lt;code&gt;Props&lt;/code&gt; with the doubles you want (an in-memory &lt;code&gt;FS&lt;/code&gt; instead of the real disk, a no-op &lt;code&gt;Logger&lt;/code&gt;, a config you&amp;rsquo;ve populated by hand) and call the constructor. No global state to reset between tests, no monkey-patching, no &lt;code&gt;init()&lt;/code&gt; order to puzzle over. The dependency is an argument, so the test just passes a different one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consistency.&lt;/strong&gt; Cross-cutting changes have exactly one place to happen. When the global &lt;code&gt;--debug&lt;/code&gt; flag flips the log level, it does so on the &lt;code&gt;Logger&lt;/code&gt; inside &lt;code&gt;Props&lt;/code&gt;, and because every command reads its logger from the same &lt;code&gt;Props&lt;/code&gt;, every command gets the new level. No command can drift, because none of them owns its own copy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extensibility.&lt;/strong&gt; Adding a new framework-wide service is just adding a field to one struct. Every command can immediately reach it; none of them needed touching to make it reachable.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Props&lt;/code&gt; is the dependency-injection container at the heart of go-tool-base: one struct, passed to every command, holding the logger, config, filesystem, assets, error handler and tool metadata. It&amp;rsquo;s a concrete struct rather than a &lt;code&gt;context.Context&lt;/code&gt; payload entirely on purpose, because dependencies belong somewhere the compiler can check them, not behind a string key and a hopeful runtime type assertion. That single choice buys you testability, consistency and easy extension.&lt;/p&gt;
&lt;p&gt;The name says it best, really. &lt;code&gt;Props&lt;/code&gt; doesn&amp;rsquo;t score the tries. It&amp;rsquo;s the broad-shouldered thing in the scrum that stops the whole framework folding, so the rest of your code is free to go and play.&lt;/p&gt;</description></item><item><title>Design your whole CLI in one file</title><link>https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/</guid><description>&lt;img src="https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/cover-design-your-whole-cli-in-one-file.png" alt="Featured image of post Design your whole CLI in one file" /&gt;&lt;p&gt;Here&amp;rsquo;s a question that sounds trivial and really isn&amp;rsquo;t: where, exactly, does a CLI tool&amp;rsquo;s &lt;em&gt;structure&lt;/em&gt; live? Not the logic of each command&amp;hellip; the structure. Which commands exist, what they&amp;rsquo;re called, which flags they take, what&amp;rsquo;s nested under what.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d never properly thought to ask it until go-tool-base forced me to, and the honest answer turned out to be a little bit embarrassing.&lt;/p&gt;
&lt;h2 id="where-does-a-clis-structure-actually-live"&gt;Where does a CLI&amp;rsquo;s structure actually live?
&lt;/h2&gt;&lt;p&gt;Picture a CLI tool with twenty commands, some nested under others. In a typical project, where does its structure live? The honest answer is &amp;ldquo;smeared across the codebase&amp;rdquo;. It&amp;rsquo;s in twenty &lt;code&gt;cmd.go&lt;/code&gt; files. It&amp;rsquo;s in the &lt;code&gt;AddCommand&lt;/code&gt; calls that stitch them together. It&amp;rsquo;s in the flag registrations. To understand the shape of the tool you have to read all of it and assemble the picture in your head, because the picture exists nowhere as a single thing you can point at.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a strange state of affairs for the single most important design fact about a CLI. The command tree is the tool&amp;rsquo;s interface, it&amp;rsquo;s the thing users actually touch, and yet it hasn&amp;rsquo;t got a home.&lt;/p&gt;
&lt;h2 id="the-manifest-gives-it-one"&gt;The manifest gives it one
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s generator gives that structure a home: &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;. The manifest is a single readable file describing the command tree. Every command, its name, its short description, its flags, its place in the hierarchy, whether it carries assets or an initialiser. The shape of the whole tool, in one place you can open and read top to bottom.&lt;/p&gt;
&lt;p&gt;And the manifest isn&amp;rsquo;t documentation &lt;em&gt;about&lt;/em&gt; the project. It&amp;rsquo;s the thing the project&amp;rsquo;s wiring is generated &lt;em&gt;from&lt;/em&gt;. When you run &lt;code&gt;regenerate project&lt;/code&gt;, the generator reads the manifest and rebuilds the boilerplate to match it: the command registration, the &lt;code&gt;AddCommand&lt;/code&gt; wiring, the flag definitions. The manifest is the source of truth, and the Go wiring is its output.&lt;/p&gt;
&lt;h2 id="design-first-when-you-want-it"&gt;Design-first, when you want it
&lt;/h2&gt;&lt;p&gt;This unlocks a way of working that the smeared-across-the-codebase approach simply can&amp;rsquo;t offer. You can design the interface first, in the manifest, and let the code follow.&lt;/p&gt;
&lt;p&gt;Want to rename a command? Edit one line in the manifest, run &lt;code&gt;regenerate&lt;/code&gt;, and the rename propagates through every wiring file that ever mentioned it. Want to move a subcommand under a different parent? Change its place in the manifest hierarchy and regenerate. Want to add a flag to three related commands? Add it in the manifest, in three obvious places, and regenerate, instead of going on a little hunting expedition for three flag-registration blocks scattered across the tree.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re editing the tool&amp;rsquo;s interface as a design, in the file whose entire job is to hold that design, and the generator does the mechanical donkey-work of making the code reflect it. The thing you change is the thing that describes the structure. The code is downstream.&lt;/p&gt;
&lt;p&gt;If that shape sounds familiar, it should. It&amp;rsquo;s the same instinct behind spec-driven and test-driven development: write down what the thing should &lt;em&gt;be&lt;/em&gt; before you assemble how it works, and keep that statement of intent as a first-class, living artefact rather than a comment that quietly rots in a corner. The manifest is a spec for your command tree, and &lt;code&gt;regenerate&lt;/code&gt; is what keeps the implementation honest to it.&lt;/p&gt;
&lt;h2 id="it-doesnt-trap-you"&gt;It doesn&amp;rsquo;t trap you
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an obvious worry about any generated-from-a-manifest system: am I now locked into editing the manifest? What if I just want to open a Go file and write some Go like a normal person?&lt;/p&gt;
&lt;p&gt;You can. The generator is careful not to own everything. It owns the wiring (the registration and the structural boilerplate) and it leaves your command logic well alone. The &lt;code&gt;RunE&lt;/code&gt; function where your command actually does its work is yours; the manifest hasn&amp;rsquo;t got an opinion about it. And the generator tracks the files it produces by content hash, so if you do hand-edit something it generated, regeneration notices and asks before overwriting rather than steamrolling you. That mechanism turned out interesting enough to get &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;its own post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So the manifest is an option, not a cage. Design-first via the manifest when that suits the change. Drop into Go directly when that suits it better. The two stay in sync because regeneration reconciles them, not because one of them has been forbidden.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;A CLI&amp;rsquo;s command tree is its most important design surface, and in most projects it has no single home&amp;hellip; it gets reconstructed in your head from twenty scattered files every time you need to reason about it. go-tool-base gives it one: &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, a readable description of the whole tree that the generator rebuilds the wiring code from. Edit the manifest, run &lt;code&gt;regenerate&lt;/code&gt;, and the boilerplate follows.&lt;/p&gt;
&lt;p&gt;It makes CLI structure something you design in one place, in the spirit of spec-driven development, while still leaving you free to write Go directly when that&amp;rsquo;s the better tool for the job. The manifest is the spec for your interface. The generator just keeps the code faithful to it.&lt;/p&gt;</description></item><item><title>Scaffolding that respects your edits</title><link>https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/</guid><description>&lt;img src="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/cover-scaffolding-that-respects-your-edits.png" alt="Featured image of post Scaffolding that respects your edits" /&gt;&lt;p&gt;When I &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduced go-tool-base&lt;/a&gt; I made a passing promise to come back to &amp;ldquo;the generator that won&amp;rsquo;t clobber your edits&amp;rdquo;. This is me keeping it, partly because it&amp;rsquo;s the feature I&amp;rsquo;m quietly most proud of, and partly because it took the most head-scratching of anything to get right.&lt;/p&gt;
&lt;p&gt;The problem it solves is one that every code generator runs into eventually, usually the hard way and usually at the worst possible moment.&lt;/p&gt;
&lt;h2 id="the-generators-awkward-second-act"&gt;The generator&amp;rsquo;s awkward second act
&lt;/h2&gt;&lt;p&gt;A project generator has an easy first act. &lt;code&gt;gtb generate skeleton&lt;/code&gt;, and you&amp;rsquo;ve got a complete, wired, idiomatic Go CLI project. Everyone&amp;rsquo;s happy, me included.&lt;/p&gt;
&lt;p&gt;The second act is the hard one. The framework moves on. A convention changes, a new built-in capability appears, the recommended CI shape shifts. Your project, scaffolded three months ago, is now subtly out of date, and you&amp;rsquo;d quite like the generator to drag it back up to spec.&lt;/p&gt;
&lt;p&gt;Except by now it isn&amp;rsquo;t a fresh scaffold. It&amp;rsquo;s &lt;em&gt;your&lt;/em&gt; project. You tuned the CI workflow. You rewrote the &lt;code&gt;justfile&lt;/code&gt;. You added a stanza to the Dockerfile that took an afternoon and a fair bit of swearing to get right. The generated files and your edited files are one and the same files.&lt;/p&gt;
&lt;p&gt;A naive generator handles this with breathtaking confidence: it regenerates everything from the template and overwrites the lot. Run it once, lose your afternoon. You learn that lesson exactly once and then never run regeneration again, which means the upkeep feature you were sold is dead on arrival. A scaffold you can&amp;rsquo;t safely re-run is just a one-shot &lt;code&gt;cp&lt;/code&gt; with extra steps.&lt;/p&gt;
&lt;h2 id="what-the-generator-needs-to-know"&gt;What the generator needs to know
&lt;/h2&gt;&lt;p&gt;The thing standing between &amp;ldquo;safe to overwrite&amp;rdquo; and &amp;ldquo;absolutely do not&amp;rdquo; is a single fact: has this file changed since the generator last wrote it?&lt;/p&gt;
&lt;p&gt;If it hasn&amp;rsquo;t, the file is still pristine boilerplate and the generator owns it. Overwrite away. If it has, a human has been in there, and the generator must not touch it without asking first.&lt;/p&gt;
&lt;p&gt;The generator can&amp;rsquo;t just eyeball that, of course. It needs a record. So every time &lt;code&gt;gtb generate&lt;/code&gt; writes a file, it computes a SHA-256 of the content and stores it in the project&amp;rsquo;s manifest, &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, as a &lt;code&gt;Hashes&lt;/code&gt; map of relative path to hash. The manifest is the generator&amp;rsquo;s memory of the exact bytes it last produced.&lt;/p&gt;
&lt;h2 id="regeneration-becomes-a-three-way-decision"&gt;Regeneration becomes a three-way decision
&lt;/h2&gt;&lt;p&gt;With that record in hand, regeneration stops being &amp;ldquo;overwrite everything&amp;rdquo; and becomes a per-file decision with three branches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file doesn&amp;rsquo;t exist.&lt;/strong&gt; Easy. Write it, store its hash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file exists and its current hash matches the manifest.&lt;/strong&gt; It&amp;rsquo;s byte-for-byte what the generator last wrote, so nobody has touched it. The generator owns it outright, regenerates from the template and updates the stored hash. No prompt, no fuss. This is the common case, and it&amp;rsquo;s silent precisely because it&amp;rsquo;s safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file exists and its hash does &lt;em&gt;not&lt;/em&gt; match.&lt;/strong&gt; Someone has been in there since generation. The generator stops and asks. It will not silently overwrite your hard-won afternoon. You decide: take the new version, or keep yours.&lt;/p&gt;
&lt;p&gt;The detail I&amp;rsquo;m genuinely fond of is what happens when you decline. Declining is non-fatal. Generation carries on with the rest of the files, and the manifest &lt;em&gt;keeps&lt;/em&gt; the file&amp;rsquo;s stored hash rather than dropping it. That matters more than it looks, because it means the file stays tracked. Next time you regenerate, the generator can still tell that file has been modified, and still asks. Skipping a file once doesn&amp;rsquo;t quietly evict it from the generator&amp;rsquo;s awareness forever. It stays a known, watched, customised file across every future run.&lt;/p&gt;
&lt;h2 id="when-you-want-it-to-stop-asking"&gt;When you want it to stop asking
&lt;/h2&gt;&lt;p&gt;Per-file prompting is the right default, but for files you&amp;rsquo;ve &lt;em&gt;permanently&lt;/em&gt; taken ownership of, being asked on every single regeneration is just noise. If you&amp;rsquo;ve rewritten the CI workflows wholesale and you are never, ever going back to the generated version, you don&amp;rsquo;t want a prompt. You want the generator to leave them well alone and not bring it up again.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what &lt;code&gt;.gtb/ignore&lt;/code&gt; is for. It sits next to the manifest and takes gitignore-style patterns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# I own the CI workflows now
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.github/workflows/**
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# ...except the release workflow, keep that managed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!.github/workflows/release.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# and my build config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;justfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anything matching is skipped during regeneration with no prompt at all. Patterns evaluate top to bottom and later ones win, so the negation (&lt;code&gt;!&lt;/code&gt;) behaves the way you&amp;rsquo;d expect from &lt;code&gt;.gitignore&lt;/code&gt;: exclude a whole directory, then claw one file back.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a deliberate escalation ladder. Unmodified files are handled silently. Modified files get a prompt. Files you&amp;rsquo;ve formally claimed get total silence. Each rung asks for less of your attention than the last, and you choose how far up to climb, file by file.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;A generator earns its keep twice: once when it scaffolds your project, and then continuously, every time it drags that project back up to the framework&amp;rsquo;s current shape. The second job is worth nothing if regeneration flattens your customisations, because you&amp;rsquo;ll simply stop running it, and who could blame you.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s generator gets around that by remembering. It hashes every file it writes into &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, and on regeneration it re-hashes before overwriting: unchanged files it owns and updates silently, changed files it stops and asks about, and &lt;code&gt;.gtb/ignore&lt;/code&gt; lets you mark files as permanently yours. Skipped files stay tracked, so the generator never loses sight of what you&amp;rsquo;ve made your own.&lt;/p&gt;
&lt;p&gt;The point of a scaffold isn&amp;rsquo;t the first five minutes. It&amp;rsquo;s that you can still run it in month three without holding your breath.&lt;/p&gt;</description></item><item><title>Your CLI is already an AI tool</title><link>https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/</link><pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/</guid><description>&lt;img src="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/cover-your-cli-is-already-an-ai-tool.png" alt="Featured image of post Your CLI is already an AI tool" /&gt;&lt;p&gt;&amp;ldquo;Make it work with AI&amp;rdquo; has become one of those requests that lands on a developer&amp;rsquo;s desk with a thud and not much further detail attached. My instinct, the first time, was to brace for a big lump of integration work&amp;hellip; a bespoke adapter for this assistant, another for that one, a treadmill of little wrappers stretching off into the distance.&lt;/p&gt;
&lt;p&gt;Turns out I&amp;rsquo;d already done most of the work. So have you, if your CLI tool is any good. Let me explain what I mean.&lt;/p&gt;
&lt;h2 id="you-already-described-your-capabilities"&gt;You already described your capabilities
&lt;/h2&gt;&lt;p&gt;Stop and think for a second about what a well-built CLI tool actually is. It&amp;rsquo;s a set of named operations, each with a human-readable description, each taking a set of typed, named, documented parameters. You wrote all of that already, because a CLI without it is unusable by &lt;em&gt;people&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Now look at what an AI assistant needs in order to call a tool. A set of named operations. A description of each, so it knows when to reach for them. A typed parameter schema for each, so it knows how to call them.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same list! A good CLI is already, structurally, a description of a set of capabilities. The information an AI agent needs isn&amp;rsquo;t extra work you have to go and do. It&amp;rsquo;s work you finished the moment your &lt;code&gt;--help&lt;/code&gt; output was any good.&lt;/p&gt;
&lt;p&gt;The only thing missing is a translator. Something that takes &amp;ldquo;this is a CLI&amp;rdquo; and presents it as &amp;ldquo;this is a set of tools an AI can call&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="mcp-is-that-translator-and-its-a-standard"&gt;MCP is that translator, and it&amp;rsquo;s a standard
&lt;/h2&gt;&lt;p&gt;The temptation, when you want your tool to be AI-usable, is to sit down and write an integration. A little adapter for Claude Desktop. Another for Cursor. Another for whatever turns up next month. Each one a bespoke wrapper, each one a thing to maintain, and the list never stops growing because new assistants keep appearing. That&amp;rsquo;s the treadmill I was bracing for.&lt;/p&gt;
&lt;p&gt;The Model Context Protocol exists to kill that list. MCP is an open standard for how an AI model discovers and calls local tools. Implement it once and your tool works with every assistant that speaks it. Write once, not once-per-client.&lt;/p&gt;
&lt;p&gt;So go-tool-base implements it once, in the framework, for everyone. (That&amp;rsquo;s rather the theme of this whole series, if you hadn&amp;rsquo;t spotted it yet&amp;hellip; do the annoying thing once, properly, in a place where every tool inherits it.)&lt;/p&gt;
&lt;h2 id="the-mcp-command-and-the-mapping-it-does-for-free"&gt;The &lt;code&gt;mcp&lt;/code&gt; command, and the mapping it does for free
&lt;/h2&gt;&lt;p&gt;Every tool built on go-tool-base inherits a built-in &lt;code&gt;mcp&lt;/code&gt; command. Run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool mcp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the tool starts a JSON-RPC server over standard I/O, speaking MCP. That&amp;rsquo;s the whole user-facing surface. One command.&lt;/p&gt;
&lt;p&gt;Behind it, the framework walks your Cobra command tree and maps it straight onto MCP tool definitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each &lt;strong&gt;command&lt;/strong&gt; becomes a &lt;strong&gt;tool&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each command&amp;rsquo;s &lt;strong&gt;short description&lt;/strong&gt; becomes the &lt;strong&gt;tool&amp;rsquo;s description&lt;/strong&gt;, the text the AI reads to decide whether this is the tool it wants.&lt;/li&gt;
&lt;li&gt;Each command&amp;rsquo;s &lt;strong&gt;flags and arguments&lt;/strong&gt; become the tool&amp;rsquo;s &lt;strong&gt;JSON Schema parameters&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s no second schema to write and then keep in sync (and we all know how well &amp;ldquo;keep these two things aligned by hand&amp;rdquo; tends to go). The command tree &lt;em&gt;is&lt;/em&gt; the schema. Add a new command to your CLI and it&amp;rsquo;s a new tool for the agent, automatically, with the description and flags you already gave it. Nobody has to remember to update an MCP manifest, because there&amp;rsquo;s no separate MCP manifest to forget about.&lt;/p&gt;
&lt;h2 id="configuring-an-assistant-to-use-it"&gt;Configuring an assistant to use it
&lt;/h2&gt;&lt;p&gt;On the assistant&amp;rsquo;s side it&amp;rsquo;s just as undramatic. You tell your AI client (Claude Desktop, Cursor, anything MCP-aware) to launch &lt;code&gt;mytool mcp&lt;/code&gt;. From then on the assistant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Starts your tool in MCP mode when it boots.&lt;/li&gt;
&lt;li&gt;Discovers every command as a callable tool.&lt;/li&gt;
&lt;li&gt;Calls the right one, with the right parameters, when a user&amp;rsquo;s request needs it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your CLI tool has quietly become something the AI can pick up and use, mid-conversation, on its own initiative.&lt;/p&gt;
&lt;h2 id="the-safety-property-worth-noticing"&gt;The safety property worth noticing
&lt;/h2&gt;&lt;p&gt;Now, &amp;ldquo;let an AI run things on my machine&amp;rdquo; is rightly a sentence that makes people nervous. It makes me nervous, and I built the thing. So it&amp;rsquo;s worth noticing the constraint sitting quietly in this design.&lt;/p&gt;
&lt;p&gt;The AI can only call what you defined. The tools it sees are exactly the commands in your tree, and the parameters it can pass are exactly the flags and arguments you declared, validated against the JSON Schema generated from them.&lt;/p&gt;
&lt;p&gt;It can&amp;rsquo;t invent a command. It can&amp;rsquo;t pass a parameter you never defined. The boundary of what the agent can do is the boundary of what your CLI does, and you drew that boundary already, back when you built the tool. Exposing the CLI over MCP doesn&amp;rsquo;t widen the surface one inch. It just makes the existing surface reachable. The AI isn&amp;rsquo;t running &lt;em&gt;things&lt;/em&gt;. It&amp;rsquo;s running &lt;em&gt;your commands&lt;/em&gt;, the ones you wrote, tested and shipped, and nothing else.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;A CLI tool, built properly, is already a structured description of a set of capabilities: named operations, descriptions, typed parameters. Which is also exactly what an AI agent needs in order to call a tool. The gap between the two is only a translator, and writing a bespoke one per assistant is a treadmill you don&amp;rsquo;t need to step onto.&lt;/p&gt;
&lt;p&gt;go-tool-base puts the translator in the framework. Every tool gets an &lt;code&gt;mcp&lt;/code&gt; command that serves the command tree over the Model Context Protocol&amp;hellip; commands become tools, descriptions become descriptions, flags become JSON Schema parameters, with no second schema to maintain. Point any MCP-aware assistant at it and your CLI is an agent-callable tool, bounded to exactly the commands you shipped.&lt;/p&gt;
&lt;p&gt;You did the hard part when you built a good CLI. MCP just opens the door you&amp;rsquo;d already framed.&lt;/p&gt;</description></item><item><title>go-tool-base: I got tired of reinventing the wheel</title><link>https://blog-570662.gitlab.io/introducing-go-tool-base/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/introducing-go-tool-base/</guid><description>&lt;img src="https://blog-570662.gitlab.io/introducing-go-tool-base/cover-introducing-go-tool-base.png" alt="Featured image of post go-tool-base: I got tired of reinventing the wheel" /&gt;&lt;p&gt;If you&amp;rsquo;ve written more than two or three command-line tools in Go, you&amp;rsquo;ll recognise the shape of the first afternoon. I certainly do! You reach for &lt;a class="link" href="https://github.com/spf13/cobra" target="_blank" rel="noopener"
 &gt;Cobra&lt;/a&gt; for the command tree, &lt;a class="link" href="https://github.com/spf13/viper" target="_blank" rel="noopener"
 &gt;Viper&lt;/a&gt; for config, and then you start the part nobody ever puts in the README&amp;hellip; the plumbing.&lt;/p&gt;
&lt;p&gt;Where does config live? A file, an env var, an embedded default? In what order do they override each other? How does the tool tell the user there&amp;rsquo;s a newer version, and how does it actually update itself? What does logging look like, and is it the same logging the next tool will use? And how do you wire all of that into each command without every command reaching into a pile of globals?&lt;/p&gt;
&lt;p&gt;None of it is hard. That&amp;rsquo;s the problem! It&amp;rsquo;s not hard, it&amp;rsquo;s just &lt;em&gt;there&lt;/em&gt;, every single time, and every single time I&amp;rsquo;d find myself reinventing it slightly differently to the last time. Different override precedence here. A subtly different update flow there. Logging that didn&amp;rsquo;t quite match the tool I&amp;rsquo;d written three months earlier. Each new tool was a fresh re-litigation of decisions I&amp;rsquo;d already made and then promptly forgotten.&lt;/p&gt;
&lt;p&gt;Now, I&amp;rsquo;ve banged on about the Boy Scout rule for years (leave the codebase better than you found it), but it has an uncomfortable corollary. If you keep turning up to the same campsite and finding it in the same mess, at some point the honest thing to do is to stop tidying it and go and build a better campsite.&lt;/p&gt;
&lt;h2 id="first-just-packages"&gt;First, just packages
&lt;/h2&gt;&lt;p&gt;So I started pulling the recurring pieces out into their own packages. Nothing grand. A config package that did the hierarchical merge the way I always ended up doing it anyway. A version package that knew how to compare semver and spot a development build. A setup package that handled first-run bootstrap and self-updating from a release. They lived as separate repos, and if you go digging through my GitHub history you can still find the scruffy ancestors of them scattered about.&lt;/p&gt;
&lt;p&gt;Separate packages was the right &lt;em&gt;first&lt;/em&gt; move. It forced each piece to stand on its own and earn its keep on a real project before I trusted it on the next one. A package that&amp;rsquo;s only ever been used in the repo it was born in hasn&amp;rsquo;t really been tested&amp;hellip; it&amp;rsquo;s just been agreed with.&lt;/p&gt;
&lt;p&gt;But separate packages come with a tax. Each one has its own release cadence, its own changelog, its own CI. Worse, they have to agree with each other at the seams, and when they&amp;rsquo;re versioned independently those seams drift. I&amp;rsquo;d bump the config package, and the setup package that depended on it would quietly need a matching bump, and the tool that used both would need telling about both. I&amp;rsquo;d traded &amp;ldquo;reinvent the wheel&amp;rdquo; for &amp;ldquo;keep a dozen wheels in sync&amp;rdquo;, and I&amp;rsquo;m really not convinced that&amp;rsquo;s a better deal.&lt;/p&gt;
&lt;h2 id="then-one-library"&gt;Then, one library
&lt;/h2&gt;&lt;p&gt;Once the packages had been used enough (used in anger, on real tools, by people who weren&amp;rsquo;t me) the shape of them stopped moving. The interfaces settled. The arguments about precedence and defaults were over, because the answers had survived contact with reality.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the point where separate packages stop being a virtue and start being friction. So I forged them into one and called it &lt;strong&gt;go-tool-base&lt;/strong&gt;. One module, one version number, one changelog, and one set of seams that are now internal and can&amp;rsquo;t drift, because they ship together.&lt;/p&gt;
&lt;p&gt;The heart of it is a dependency-injection container, a &lt;code&gt;Props&lt;/code&gt; struct, that holds the things every command needs: the logger, the config, the embedded assets, the filesystem handle, the error handler, the tool&amp;rsquo;s own metadata. Commands are handed &lt;code&gt;Props&lt;/code&gt; explicitly rather than reaching for globals, which means a command is just a function of its inputs and is therefore trivially testable. That one decision has quietly paid for itself on every tool I&amp;rsquo;ve built since.&lt;/p&gt;
&lt;p&gt;Around that container sits all the stuff I was so tired of rewriting: hierarchical config, structured logging, version checking, self-update from GitHub or GitLab releases, an interactive TUI documentation browser, AI integration, service lifecycle management. A new tool inherits the lot and gets to spend its first afternoon on the thing that&amp;rsquo;s actually novel&amp;hellip; its own logic.&lt;/p&gt;
&lt;h2 id="finally-a-generator"&gt;Finally, a generator
&lt;/h2&gt;&lt;p&gt;A library still leaves you staring at a blank &lt;code&gt;main.go&lt;/code&gt;. You still have to know the conventions, wire the container, lay out the directories, register the commands. All knowable, but all boilerplate. And boilerplate is exactly the enemy I set out to kill in the first place.&lt;/p&gt;
&lt;p&gt;So go-tool-base ships a generator. &lt;code&gt;gtb generate skeleton&lt;/code&gt; scaffolds a complete, working, idiomatic project: directory layout, the wired &lt;code&gt;Props&lt;/code&gt; container, the command tree, CI, the whole lot. &lt;code&gt;gtb generate command&lt;/code&gt; adds a new command and registers it for you. The generator also handles upkeep: when the framework&amp;rsquo;s conventions move, it can regenerate the scaffolding of an existing project without trampling all over the code you&amp;rsquo;ve written on top. (That last bit turned out to be a properly interesting problem in its own right, and a future post.)&lt;/p&gt;
&lt;p&gt;The goal is blunt. Creating a CLI tool should be about the tool, not the scaffolding. The first afternoon should be spent on the part that&amp;rsquo;s actually worth writing.&lt;/p&gt;
&lt;h2 id="one-thing-i-was-careful-about"&gt;One thing I was careful about
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a nasty failure mode with &amp;ldquo;batteries-included&amp;rdquo; frameworks: the day you outgrow them, they hold you hostage. You either stay inside the framework&amp;rsquo;s worldview forever, or you face a rewrite. I&amp;rsquo;ve been burned by that before and I had no intention of inflicting it on anyone else.&lt;/p&gt;
&lt;p&gt;So go-tool-base generates idiomatic, standard-library-compliant Go. There&amp;rsquo;s no magic runtime you can&amp;rsquo;t see, no clever code you couldn&amp;rsquo;t have written by hand. If you ever outgrow the framework the generated code stands on its own and you walk away with a perfectly normal Go project. A framework should be a starting point you&amp;rsquo;re glad you took, not a room you can&amp;rsquo;t get out of.&lt;/p&gt;
&lt;h2 id="where-this-leaves-me"&gt;Where this leaves me
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I was spending the first afternoon of every Go CLI tool rebuilding the same plumbing, and rebuilding it slightly wrong relative to last time. It started life as separate packages so each piece could earn its place on real projects; once they&amp;rsquo;d stopped moving I forged them into a single library so the seams couldn&amp;rsquo;t drift; and then I wrapped a generator around it so a new tool starts as a working project rather than a blank file.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a framework for the unglamorous 80% (config, versioning, updates, logging, lifecycle) so you can spend your time on the 20% that&amp;rsquo;s actually yours.&lt;/p&gt;
&lt;p&gt;Over the coming posts I&amp;rsquo;ll dig into the individual pieces&amp;hellip; the generator that won&amp;rsquo;t clobber your edits, the credential handling, the self-update integrity checks, and a few Go techniques I&amp;rsquo;m rather pleased with along the way. Stay tuned!&lt;/p&gt;</description></item><item><title>Migrating away from Mediawiki and how to export its data</title><link>https://blog-570662.gitlab.io/migrating-away-from-mediawiki-and-how-to-export-its-data/</link><pubDate>Wed, 19 Aug 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/migrating-away-from-mediawiki-and-how-to-export-its-data/</guid><description>&lt;p&gt;I like Mediawiki, it is a simple tool capable of doing a lot and can be very flexible and easy to customise. However its not always the right solution! I had a situation where we needed to migrate away from using it for a combination of security and usability reasons. So I thought it would be good to document it.&lt;/p&gt;
&lt;p&gt;After reviewing a few things it was decided to move things over to the companies already existing O365 SharePoint as a new site. This sounded laborious as first, but actually turned out to be pretty straight forward.&lt;/p&gt;
&lt;p&gt;We start with getting data out of Mediawiki, thankfully we only wanted the most recent revision and not the full history of a page. We use PostgreSQL as a back-end so it was reasonably straight forward to figure out how to extract the data in a sensible query.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_touched&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edited&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_revision_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_content_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OVERLAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;placing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_touched&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It tool a little sleuthing to realize that the &lt;code&gt;slots&lt;/code&gt; table was the pivotal in extracting the latest page version. With the right join and a little mangling of the &lt;code&gt;content_address&lt;/code&gt; field from the &lt;code&gt;contents&lt;/code&gt; table to remove the &amp;ldquo;tt:&amp;rdquo; from the value and convert to an integer we now have a result set of all the page names and the latest revision of that page. I also added in the date the page was last updated to allow me to see when it was last edited as it was a live system migration and helped me to ensure content remained sync while both were still in play.&lt;/p&gt;
&lt;p&gt;Once I had the query it was a simple job of writing an &amp;ldquo;Exporter&amp;rdquo; using Go Lang to extract the data and write it to files, I&amp;rsquo;ll chuck a snippet of code at the bottom of the post for you.&lt;/p&gt;
&lt;p&gt;Mediawiki uses &lt;code&gt;wikitext&lt;/code&gt; as a format so I needed to convert it to something more widely understood. Having used Pandoc in the past successfully I plumped for this as I knew it would handle a lot of options and was simple to use to convert to the &lt;code&gt;markdown_mmd&lt;/code&gt; format&lt;/p&gt;
&lt;p&gt;I Installed it via the ubuntu apt package available on my system and wired this in as a hacky &lt;code&gt;exec&lt;/code&gt; command into my script&amp;hellip; and voila! I had hardcopies of all the Mediawiki pages on my system in both &lt;code&gt;wikitext&lt;/code&gt; and &lt;code&gt;markdown_mmd&lt;/code&gt; format.&lt;/p&gt;
&lt;p&gt;Why &lt;code&gt;markdown_mmd&lt;/code&gt; I hear you ask&amp;hellip; mainly because it gave me the cleanest conversion for use with the new markdown web page widget for Sharepoint&amp;rsquo;s modern interface.&lt;/p&gt;
&lt;p&gt;Now we have the files we could do a little munging and parsing to convert URLs into the format needed for the new location in Sharepoint, easily done with a bit of regex pattern matching, which I wont go into as yours will be very different from mine&amp;hellip; suffice to say looking for &lt;code&gt;&amp;quot;wikilink&amp;quot;&lt;/code&gt; in my regex helped massively in finding all the occurrences I needed to update. I used &lt;code&gt;sed&lt;/code&gt; but you could use whatever tool you like or add it into your version of the exporter&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;SysAdmin/(.+) &amp;#34;wikilink&amp;#34;&amp;#39; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and with a little back referencing to substitute the values we need to keep and its all good.&lt;/p&gt;
&lt;p&gt;Next came the import of the data into Sharepoint, but that is a post for another day.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;bytes&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;github.com/jmoiron/sqlx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;github.com/rs/zerolog/log&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;io/ioutil&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;os/exec&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;path/filepath&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;wiki-export/src/util&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="ne"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Edited&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Exporter&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Config&lt;/span&gt; &lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExporterConfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Exporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Export&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_text&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_touched&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;edited&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_revision_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_content_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OVERLAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_address&lt;/span&gt; &lt;span class="n"&gt;placing&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;page_touched&lt;/span&gt; &lt;span class="n"&gt;DESC&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queryx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiFilename&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;.mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdFilename&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;.md&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiDir&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdDir&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetFormat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;wikiDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/mediawiki/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;mdDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/md/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MkdirAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wikiDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MkdirAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mdDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiTarget&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiFilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdTarget&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdFilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Msgf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pandoc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-f&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-t&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errorBuffer&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputBuffer&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;outputBuffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;errorBuffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Msgf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ERROR: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mdTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Pre-populating Neo4J using Kubernetes Init Containers and neo4j-admin import</title><link>https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/</link><pubDate>Wed, 15 Jul 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/</guid><description>&lt;img src="https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/maxresdefault.jpg" alt="Featured image of post Pre-populating Neo4J using Kubernetes Init Containers and neo4j-admin import" /&gt;&lt;p&gt;Recently there has been an uptake in the use of Neo4j by the Data Scientists. This is a good thing! they are wanting to use the right tool for the job. However we need to run it inside our k8s cluster as a portable readable data source that has been dynamically populated from a pile of data in a combination of PostgreSQL and MongoDB.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a problem for them working locally, they install and spin up a local copy of Neo4j and can interact with it quite happily. They even realised that they can generate CSV&amp;rsquo;s from PostgreSQL and MongoDB and then import them, blindingly fast, into Neo4j using the &lt;code&gt;neo4j-admin&lt;/code&gt; tool that comes bundled. Fantastic!&lt;/p&gt;
&lt;p&gt;At least until they come to want to run their Neo instance inside our k8s cluster. That&amp;rsquo;s where I step in and turn them aside from creating their own custom neo4j image with a bespoke entry point that loads all the data for them in some crazy threaded bash scripting!&lt;/p&gt;
&lt;p&gt;&amp;ldquo;No, No, No!&amp;rdquo; I tell them. &amp;ldquo;It&amp;rsquo;s far easier to just add an init container to your pod, that will preload the data before Neo starts up&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Init containers, if you haven&amp;rsquo;t come across before, them are a special type of container that lives inside a k8s pod and are set to run &lt;strong&gt;BEFORE&lt;/strong&gt; your main container runs. In this case it means we can easily sequence a bash script to run the &lt;code&gt;neo4j-admin import&lt;/code&gt; before Neo4j is even started. And here is how we did it!&lt;/p&gt;
&lt;h2 id="the-script"&gt;The script
&lt;/h2&gt;&lt;p&gt;The data scientists had been using Neo4j 3.5.x locally because they had a need for the graph algorithms plugin (&lt;a class="link" href="https://github.com/neo4j-contrib/neo4j-graph-algorithms" target="_blank" rel="noopener"
 &gt;https://github.com/neo4j-contrib/neo4j-graph-algorithms&lt;/a&gt;) which at the time they were looking didn&amp;rsquo;t support Neo4j 4.x. The plugin is now deprecated and its replacement (&lt;a class="link" href="https://github.com/neo4j/graph-data-science" target="_blank" rel="noopener"
 &gt;https://github.com/neo4j/graph-data-science&lt;/a&gt;) thankfully supports 3.5.x and 4.x.&lt;/p&gt;
&lt;p&gt;As Neo4j 4.x introduces a lot of new features and improves performance so I recommended we switch to using that. This meant a refactor of their bash script for neo4j-admin there some very subtle differences and a few caveats to work with. This is what they came up with&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DBNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;neo4j&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -eq &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;DBNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# extract data from SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 extract_data.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remove old db for rebuild&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf &lt;span class="s2"&gt;&amp;#34;/data/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DNBAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;neo4j-admin import &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --database&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --delimiter&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;|&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;Protein&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_protein_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_proteins.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;UniProtKB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_uniprot_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_uniprot.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;HAS_AMINO_ACID_SEQUENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EDGE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_protein_sequence_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_protein_sequence.csv &lt;span class="se"&gt;\ &lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;HAS_AMINO_ACID_SEQUENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EDGE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_chembl_protein_biotherapeutic_molregno_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_chembl_protein_biotherapeutic_molregno.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --skip-bad-relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --skip-duplicate-nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;import&lt;/code&gt; command here is significantly shorter for example purposes, as the original is about 120 lines long. As you can see it&amp;rsquo;s pretty straight forward, they had another script in &lt;code&gt;extract_data.py&lt;/code&gt;, that I wont bore you with suffice to say that it pulled out all the data they wanted from PostgreSQL and MongoDB, which got saved to disk as CSV files in the relevant directories.&lt;/p&gt;
&lt;p&gt;Great, it worked on their local version!&lt;/p&gt;
&lt;h2 id="the-dockerfile"&gt;The Dockerfile
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ROM neo4j:latest
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4JLABS_PLUGINS [&amp;#34;graph-data-science&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUN apt update &amp;amp;&amp;amp; apt install -y python3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WORKDIR /srv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY src /srv/src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY headers /srv/headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The plan is always to keep it simple. We have one image that we can run for both the init container and the main container. This docker file gives a vanilla neo4j instance with python and our scripts for extracting the data loaded into it&lt;/p&gt;
&lt;h2 id="the-k8s-manifest"&gt;The k8s Manifest
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Pod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;NEO4J_AUTH&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j/password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;registry.example.com/phpboyscout/rnd_graph:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Always&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;initContainers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;importer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;neo4j_import.sh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/bin/bash&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/import/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;HEADER_DIR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/srv/headers&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;registry.example.com/phpboyscout/rnd_graph:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Always&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;workingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/srv/src&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/import&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;import&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;claimName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can pull it all together with our k8s manifest. From here you can see that we have our default neo4j container that we pass in our default authentication details to and an init container that runs our &lt;code&gt;import.sh&lt;/code&gt; script. Both containers have access to a shared volume for the &lt;code&gt;/import&lt;/code&gt; and &lt;code&gt;/data&lt;/code&gt; folders.&lt;/p&gt;
&lt;p&gt;And now we get to&amp;hellip;&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting
&lt;/h2&gt;&lt;p&gt;So right off the bat it didn&amp;rsquo;t work! No surprises there but here are a few things that caused us some issues and how we resolved them.&lt;/p&gt;
&lt;h3 id="database-offline"&gt;Database offline
&lt;/h3&gt;&lt;p&gt;At first glance everything seemed to work. Until we tried to connect to the &lt;code&gt;neo4j&lt;/code&gt; database with the default UI, at which point we were presented with the error message&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Database &amp;#34;neo4j&amp;#34; is unavailable, its status is &amp;#34;offline.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This took a little sleuthing and shelling into the neo4j container to take a look at the &lt;code&gt;/var/debug.log&lt;/code&gt; file which gives significantly more useful information about whats going on with the server. First we were getting stack traces that contained messages like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Component &amp;#39;org.neo4j.kernel.impl.transaction.log.files.TransactionLogFiles@59d6a4d1&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;was successfully initialized, but failed to start. Please see the attached cause 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exception &amp;#34;/data/transactions/neo4j/neostore.transaction.db.0&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From experience this sounded like a permissions issue and lo and behold, checking the files on the filesystem showed that because the import script was run as root the database files were owned by root. We resolved this by adding:-&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R neo4j:neo4j /data/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to the bottom of the import script. Next we were then presented with an error that looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2020-07-14 16:56:33.919+0000 WARN [o.n.k.d.Database] [neo4j] Exception occurred while
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;starting the database. Trying to stop already started components. Mismatching store id.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one seems like it would be an obvious one to google and I did come up with few pages that seemed to describe what was happening to me but gave some varied solutions, from starting and stopping the sever and running &lt;code&gt;neo4j-admin unbind&lt;/code&gt; in between to deleting various files. It seemed very strange because we did test this with the 3.5.17 version of Neo and it worked fine.&lt;/p&gt;
&lt;p&gt;The solution we needed was to wipe the slate clean properly. The line in our script to remove the previous build of the db&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remove old db for rebuild&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf &lt;span class="s2"&gt;&amp;#34;/data/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DNBAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;just didn&amp;rsquo;t cut it. It turns out that because the 4.x version of Neo4j supports multiple databases the &lt;code&gt;import&lt;/code&gt; command writes additional information to the system database and transactions database in the form of some identifiers for each database, BUT if you don&amp;rsquo;t do something to clear that value for the database your are building it wont match up when the server starts and you get a declaration of &lt;code&gt;Mismatching store id&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if the developers are aware of this flaw, so in the mean time we have to expand our cleanup to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# clean up for fresh import&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf /data/databases/*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf /data/transactions/*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;removing the neoj4, system and store_lock databases and transaction logs from the data store. This solved the problem and the server was able to start and we could connect to neo4j database successful.&lt;/p&gt;
&lt;p&gt;Its not an ideal solution, I can foresee definite situations we will have to work around when we get to a point where multiple databases may be needed and are built separately and independently from each other. but it will suffice for now.&lt;/p&gt;
&lt;h3 id="malloc-error-message-goes-here"&gt;Malloc(): Error message goes here
&lt;/h3&gt;&lt;p&gt;Once it was up and running we noticed that we were getting lots of restarts on the main neo4j container a quick look at the &lt;code&gt;stdout&lt;/code&gt; log and we could see each restart ending with something that looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;malloc(): corrupted top size
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;instantly this looks like an issue with memory sizing inside the container for the JVM. Thankfully the team at Neo4j have accounted for this and give you a nice tool in the form of&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;neo4j-admin memrec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which interrogates the databases and gives some sensible values you can set in the output which in our case looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Memory settings recommendation from neo4j-admin memrec:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Assuming the system is dedicated to running Neo4j and has 376.6GiB of memory,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# we recommend a heap size of around 31g, and a page cache of around 331500m,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# and that about 22400m is left for the operating system, and the native memory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# needed by Lucene and Netty.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: If the indexing storage use is high, e.g. there are many indexes or most&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# data indexed, then it might advantageous to leave more memory for the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# operating system.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: Depending on the workload type you may want to increase the amount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# of off-heap memory available for storing transaction state.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# For instance, in case of large write-intensive transactions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# increasing it can lower GC overhead and thus improve performance.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On the other hand, if vast majority of transactions are small or read-only&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# then you can decrease it and increase page cache instead.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: The more concurrent transactions your workload has and the more updates&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# they do, the more heap memory you will need. However, don&amp;#39;t allocate more&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# than 31g of heap, since this will disable pointer compression, also known as&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# &amp;#34;compressed oops&amp;#34;, in the JVM and make less effective use of the heap.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: Setting the initial and the max heap size to the same value means the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# JVM will never need to change the heap size. Changing the heap size otherwise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# involves a full GC, which is desirable to avoid.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Based on the above, the following memory settings are recommended:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;331500&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# It is also recommended turning out-of-memory errors into full crashes,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# instead of allowing a partially crashed database to continue running:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#dbms.jvm.additional=-XX:+ExitOnOutOfMemoryError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# The numbers below have been derived based on your current databases located at: &amp;#39;/var/lib/neo4j/data/databases&amp;#39;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# They can be used as an input into more detailed memory analysis.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Total size of lucene indexes in all databases: 0k&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Total size of data and native indexes in all databases: 17300m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So how to get these values into the container&amp;hellip; Thankfully this is handled for you in the form of Environment Variables you can pass into the docker image. A bit of a google and i found this little snippet which is a goldmine for telling us how to translate settings into environment variables.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Env variable naming convention:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - prefix NEO4J_&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - double underscore char &amp;#39;__&amp;#39; instead of single underscore &amp;#39;_&amp;#39; char in the setting name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - underscore char &amp;#39;_&amp;#39; instead of dot &amp;#39;.&amp;#39; char in the setting name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dbms.tx_log.rotation.retention_policy setting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As for getting the variables into the container, you could do this from the pod and inject it in. I this case because the data we are going to be using is reasonably stable and tested we decided to stick them into the Docker file with the &lt;code&gt;ENV&lt;/code&gt; directive.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_heap_initial__size 31g
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_heap_max__size 31g
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_pagecache_size 331500m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And so far we haven&amp;rsquo;t had a restart yet!&lt;/p&gt;</description></item><item><title>Adding Ambient Sounds to your Discord Server On LInux</title><link>https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/</link><pubDate>Tue, 30 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/</guid><description>&lt;img src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/tfOnZwZBwA-e1593539639171.jpg" alt="Featured image of post Adding Ambient Sounds to your Discord Server On LInux" /&gt;&lt;p&gt;I&amp;rsquo;m a Dungeon Master! I don&amp;rsquo;t mean that in the S&amp;amp;M sense! As in the game Dungeons &amp;amp; Dragons (&lt;a class="link" href="https://dnd.wizards.com" target="_blank" rel="noopener"
 &gt;https://dnd.wizards.com&lt;/a&gt;), where I run a weekly game as well as take part in a couple of campaigns as a player. It&amp;rsquo;s a lot of fun and something I would definitely recommend you have a go at if you are so inclined&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://discord.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="245px" data-flex-grow="102" height="240" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/2c21aeda16de354ba5334551a883b481.png" width="245"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is a vast amount of tooling &amp;amp; tech out there that allows you to play remotely such as Virtual Table Tops, Character builders, online resources, etc. One such tool that gets used quite often is a chat service called Discord (&lt;a class="link" href="https://discord.com" target="_blank" rel="noopener"
 &gt;https://discord.com&lt;/a&gt;) It&amp;rsquo;s really useful and allows you to easily be part of and manage communities of people&amp;hellip;. Think IRC &amp;amp; Slack, but more up to date than IRC and less &amp;ldquo;workish&amp;rdquo; than Slack.&lt;/p&gt;
&lt;p&gt;As part of my online games I like being able to have ambient music to match the surroundings the players are traveling through, as well as some active elements thrown in for good measure. This is possible in a few different ways using discord but the way I want to set it up can be somewhat frustrating to set up. Let me explain:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://syrinscape.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="709px" data-flex-grow="295" height="114" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/logo-1.jpg" width="337"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have taken a shine to two tools in particular&amp;hellip; Syrinscape (&lt;a class="link" href="https://syrinscape.com" target="_blank" rel="noopener"
 &gt;https://syrinscape.com&lt;/a&gt;) and Table Top Audio (&lt;a class="link" href="https://tabletopaudio.com" target="_blank" rel="noopener"
 &gt;https://tabletopaudio.com&lt;/a&gt;). The former being a windows app with an nice interactive mixing UI that allows you to combine and generate unique sounds, the latter being a lovely web service that has some fantastic loop-able ambient background tracks all 100% free.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://tabletopaudio.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="1008px" data-flex-grow="420" height="120" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/header3.png" width="504"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I am wanting to be able to pipe the audio from these two services into my Discord server so that I can make use of the fantastic audio they offer. This is the journey of how I managed to get this working, partly as a reminder for me if I ever need to do this again and also to help others that may be looking to do the same.&lt;/p&gt;
&lt;h2 id="my-setup"&gt;My Setup
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been a big fan of Ubuntu for a number of years, but since 20.04 I&amp;rsquo;ve found that the shine I&amp;rsquo;ve had for it has waned significantly. I wont go into the why and wherefore of it but I&amp;rsquo;m now running the excellent Pop_OS! from System76 (&lt;a class="link" href="https://pop.system76.com" target="_blank" rel="noopener"
 &gt;https://pop.system76.com&lt;/a&gt;) its an Ubuntu variant but with the bits I dislike removed. So assume that anything I&amp;rsquo;m doing is compatible with Ubuntu 20.04.&lt;/p&gt;
&lt;h2 id="the-requirements"&gt;The Requirements
&lt;/h2&gt;&lt;p&gt;The ideal solution should see me being able to have a single instance of discord running that allows me to still use my mic to be able to talk, and to have my selected background playing with the ability to control the volumes of both the mic and the background independently.&lt;/p&gt;
&lt;h2 id="finding-a-solution"&gt;Finding A Solution
&lt;/h2&gt;&lt;p&gt;A lot of googling led me to realise that there isn&amp;rsquo;t a perfect solution to fit my brief. The hardest part being not actually knowing what to google and a lot of the terminology being somewhat foreign to me as I&amp;rsquo;m not much of an audio engineer. However I finally stumbled upon a &lt;a class="link" href="https://endless.ersoft.org/pulseaudio-loopback/" target="_blank" rel="noopener"
 &gt;blog post&lt;/a&gt; by Emma Anderson dated June 2016 and thankfully it gives me a lot of the heavy lifting that I needed along with some explanation of what I&amp;rsquo;m trying to achieve, though I&amp;rsquo;m hopefully going to be more verbose here in what this all means and how it works.&lt;/p&gt;
&lt;h2 id="pulseaudio"&gt;PulseAudio
&lt;/h2&gt;&lt;p&gt;The first thing we need to do is make sure the packages for &lt;code&gt;pulseaudio&lt;/code&gt; and &lt;code&gt;pavucontrol&lt;/code&gt; are installed. These will allow us to manipulate the way we capture sound and redirect it to the appropriate inputs and outputs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install pulseaudio pavucontrol
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="virtual-input--virtual-mic"&gt;Virtual Input &amp;amp; Virtual Mic
&lt;/h2&gt;&lt;p&gt;What we are going to try to achieve, is to create two new elements inside of Pulseaudio;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a Virtual input that we can channel the applications creating our background sounds which will allow us to control the volume independently.&lt;/li&gt;
&lt;li&gt;a Virtual Microphone that we can channel our both our normal microphone and the new Virtual input into.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By creating these elements we can then use the &lt;code&gt;pavucontrol&lt;/code&gt; tool to select what needs to be redirected where. so lets get started.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualInput device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualInput.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput.monitor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we have two commands, the first will create our new Virtual Input as what is referred to as a &amp;ldquo;null sink&amp;rdquo;. This on its own is not really very useful for us as we also need what is referred to as a &amp;ldquo;source&amp;rdquo;, thankfully when we run this command it also created a new &amp;ldquo;source&amp;rdquo; for us.&lt;/p&gt;
&lt;p&gt;On it&amp;rsquo;s own that should be more than enough, but running the 2nd &amp;amp; 3rd command makes our live a lot easier because it will apply some very useful labels to both of the newly created sink and source. In this case &lt;code&gt;VirtualInput&lt;/code&gt; for the sink and &lt;code&gt;VirtualInput.monitor&lt;/code&gt; for the source. Having these in place makes it a lot simpler to configure things with &lt;code&gt;pavucontrol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next we need to create our Virtual Mic using some very familiar looking commands.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;VM&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="n"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pacmd&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;proplist&lt;/span&gt; &lt;span class="n"&gt;VirtualMic&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pacmd&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;proplist&lt;/span&gt; &lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;again we have now created a new new pair of sink and source with some nice easy to recognise labels that we will use when we start working with &lt;code&gt;pavucontrol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next piece to our puzzle is creating the components that will let us define a connection from the &lt;code&gt;VirtualInput&lt;/code&gt; and our physical microphone to the newly created &lt;code&gt;VirtualMic&lt;/code&gt;. We do this with two identical commands;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;loopback&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;loopback&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;we now have most of the elements that we need to configure everything to work.&lt;/p&gt;
&lt;h2 id="listening-to-my-own-ambience"&gt;Listening to my own Ambience
&lt;/h2&gt;&lt;p&gt;Before we can start wiring it all together we need to ensure we can also listen back to our own ambience. This involves us creating one more &amp;ldquo;loopback&amp;rdquo; module that points to the speakers we are wanting to listen to. Lets find out what our options are by running;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd list-sinks &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="err"&gt;&amp;#39;&lt;/span&gt;/index:/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; /name:/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; /device.description/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This lists all of the available &amp;ldquo;sinks&amp;rdquo; that we can use. on my daily driver laptop I get;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * index: &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	name: &amp;lt;alsa_output.pci-0000_00_1f.3.analog-stereo&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		device.description &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Built-in Audio Analogue Stereo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells us the &amp;ldquo;index&amp;rdquo; for the device, its name and also some kind of description. The important bit for us here is the name as we will need that to create our new &amp;ldquo;loopback&amp;rdquo; with the command;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alsa_output.pci-0000_00_1f.3.analog-stereo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates the last piece for our puzzle!&lt;/p&gt;
&lt;h2 id="connecting-it-all-together"&gt;Connecting it all together
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m now going to assume you have logged yourself into the Discord client and fired up your copy of Syrinscape&amp;hellip; but you should just as easily swap out these for something else of your choice.&lt;/p&gt;
&lt;p&gt;Now we can start &lt;code&gt;pavucontrol&lt;/code&gt; either from the command line or you can look for it in your applications menu. Once it loads you will hopefully be presented with something that looks like;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="317px" data-flex-grow="132" height="644" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23_hu_56270fcef40843d0.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23.png 853w" width="853"&gt;&lt;/p&gt;
&lt;p&gt;For this next step I am specifically starting on the &amp;ldquo;Recording&amp;rdquo; tab of &lt;code&gt;pavucontrol&lt;/code&gt; this is to allow us to set up what is going to be captured. I have updated the drop-down at the bottom left to show &amp;ldquo;All Streams&amp;rdquo; as this will make it quicker to configure… Starting at the top we have two entries for;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback to VirtualMic from&lt;/strong&gt;: These are the result of the first two &amp;ldquo;loopback&amp;rdquo; modules we created with the &lt;code&gt;pactl&lt;/code&gt; command we ran previously. They are going to allow us to capture the audio streams from our physical microphone, mine here is the &lt;code&gt;TONOR TC-777&lt;/code&gt; and our newly created &lt;code&gt;VirtualMic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;followed by a single entry for;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback to Built-in-Audio Analougue Stereo from&lt;/strong&gt;: which is the last &amp;ldquo;loopback&amp;rdquo; module that we create to let us hear our own Ambience, Having this set to our &lt;code&gt;VirtualInput&lt;/code&gt; means that anything that pipe into our &lt;code&gt;VirtualInput&lt;/code&gt; will also come out of our speakers.&lt;/p&gt;
&lt;p&gt;and finally;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt;: Once you connect to a voice channel in discord this will appear and it allows us to specify which of our devices it should be reading the audio feed from. For our purposes we have this set to our VirtualMic so that we can have our mixed audio feeds&lt;/p&gt;
&lt;p&gt;Now that recording is configured we can sort out our playback.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="203px" data-flex-grow="84" height="1003" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57_hu_b9e9d12b3e2e9294.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57.png 850w" width="850"&gt;&lt;/p&gt;
&lt;p&gt;Here we can see the &amp;ldquo;Playback&amp;rdquo; tab of &lt;code&gt;pavucontrol&lt;/code&gt;, again set to show &amp;ldquo;All Streams&amp;rdquo;. This time I&amp;rsquo;m going to run through the elements here starting from the bottom of the list and working my way up&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt;: This again is our connection to a Discord voice channel, as you can see I have this set to play back all of its output via &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; which is how my Operating system has labelled my physical speakers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Syrinscape.exe&lt;/strong&gt;: This is the Syrinscape application, that I run through PlayOnLinux (&lt;a class="link" href="https://www.playonlinux.com" target="_blank" rel="noopener"
 &gt;https://www.playonlinux.com&lt;/a&gt;), and I will use to generate all of my lovely ambient sounds. This is set to play all of its &amp;ldquo;audio stream&amp;rdquo; on our &lt;code&gt;VirtualInput&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next two items in our list, providing you have configured the Recording tab first should look as in the image. Changing the &amp;ldquo;Loopback to VirtualMic&amp;rdquo; entries on the Recording tab will change the labels of these two entries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback of VirtualInput.monitor on&lt;/strong&gt;: it seems we have two of these entries and where we can tell the to pip all of the audio we are now capturing on our &lt;code&gt;VirtualInput&lt;/code&gt; In this case we want it to go to two places, our &lt;code&gt;VirtualMic&lt;/code&gt; so that it can be sent to both our Discord audio channel and also to our &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; speakers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback of Built-in Audio Analogue Stereo on&lt;/strong&gt;: is where we now direct the input from our physical microphone and feed that straight into our &lt;code&gt;VirtualMic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The other entries in the list here are for firefox and the system itself and are not relevant to what we are trying to achive.&lt;/p&gt;
&lt;h2 id="winner-winner-chicken-dinner"&gt;Winner Winner Chicken Dinner
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s is effectively all we need to do&amp;hellip; From here on in anything you play via the Syrinscape app will be merged with your microphone input and passed to Discord. You can then use the volume sliders in &lt;code&gt;pavucontrol&lt;/code&gt; to adjust the levels of all the inputs to suit your own personal preference.&lt;/p&gt;
&lt;p&gt;Though I will make a few small suggestions about how to configure your discord settings. You shouldn&amp;rsquo;t need to make any adjustments to the input and output devices which should now be set to &lt;code&gt;Default&lt;/code&gt; as your &amp;ldquo;Input Device&amp;rdquo; if you change this it will override the changes we have made and you will need to go back to the Recording tab of &lt;code&gt;pavucontrol&lt;/code&gt; and switch &lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt; back to InputMic , but&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="322px" data-flex-grow="134" height="692" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02_hu_f7eba952aec4ac92.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02.png 931w" width="931"&gt;&lt;/p&gt;
&lt;p&gt;I would recommend disabling automatic input sensitivity and lowering the sensitivity slider all the way down to -100dB&amp;hellip; this is to allow for the potential low and subtle tones and ambient elements you may want to play&amp;hellip; be warned though it makes it very very easy for an low quality microphone (such as the &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; microphone found on my laptop) to pick up other noises such as your systems fans, mouse clicks and typing. A simple way to combat this is to get a reasonable quality external cardioid condenser microphone which eliminates a lot of this unwanted background.&lt;/p&gt;
&lt;h2 id="one-last-thing"&gt;One last thing
&lt;/h2&gt;&lt;p&gt;That should be it for now&amp;hellip; I&amp;rsquo;ll leave you with one final thing. This is a simple little bash script I threw together that I run in can a terminal to create all the components and if I want will then clean them all up and remove them. If you really want you could set it up as a permanent implementation, but I&amp;rsquo;ll let you google for that solution!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LB1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenback&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Listing all possible output devices&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pacmd list-sinks &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;/index:/ {print $0}; /name:/ {print $0}; /device\.description/ {print $0}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Please enter the name of the output device to create a loopback for (leave blank to skip): &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;read&lt;/span&gt; S1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating Loopback for &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nv"&gt;LB1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; true&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;read&lt;/span&gt; -p &lt;span class="s2"&gt;&amp;#34;Finished? do you want to clean up and remove modules [Yn]: &amp;#34;&lt;/span&gt; yn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$yn&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt;Yy&lt;span class="o"&gt;]&lt;/span&gt;* &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; 0&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt;Nn&lt;span class="o"&gt;]&lt;/span&gt;* &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; 1&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Please answer yes or no.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenback
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating VirtualInput&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualInput device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualInput.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput.monitor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating VirtualMic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualMic device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualMic.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic.monitor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating loopbacks for VirtualMic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VML1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VML2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;All modules have been loaded have been configured! Run pavucontrol to configure your devices.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; cleanup&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$LB1&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LB1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;All modules have been unloaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$LB1&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Modules &lt;/span&gt;&lt;span class="nv"&gt;$LB1&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp; &lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt; remain loaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Modules &lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt;, &amp;amp; &lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt; remain loaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Encrypting additional drives with LUKS on Linux</title><link>https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/</link><pubDate>Mon, 29 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/</guid><description>&lt;img src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing.jpg" alt="Featured image of post Encrypting additional drives with LUKS on Linux" /&gt;&lt;p&gt;Encryption is king nowadays with everyone having mobile devices. We have a significant number of people on laptops that travel around and also workstations that live in open plan offices. This means we encrypt all of our disks&amp;hellip; just in case. 99% of the time is super simple to do as most OS installers give you the option to do it, some now ven enforce it as a default option. This post however is about adding an additional disk to the system and making it automatically mount on system startup.&lt;/p&gt;
&lt;p&gt;So let me set the scene, we have a data-scientist that&amp;rsquo;s running out of disk space for a task they are running on their Ubuntu 18.04 Workstation. At some point the workstation had an upgrade to the HDD in the past to a shiny new SSD, and the old 4Tb spinning disk was left in the chassis that they want to use for this very specific task.&lt;/p&gt;
&lt;p&gt;Now this workstation has been through a couple of data-scientists over the last 12 months and unfortunately the LUKS password that had been set up for the old spinning disk has gone walkabouts&amp;hellip; so the plan is as follows&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;flatten the old disk and set up a new partition using the whole disk&lt;/li&gt;
&lt;li&gt;generate a new secure encryption key&lt;/li&gt;
&lt;li&gt;set up LUKS encryption on the new partition&lt;/li&gt;
&lt;li&gt;use Ext4 as a filesystem&lt;/li&gt;
&lt;li&gt;enable auto decryption of the disk&lt;/li&gt;
&lt;li&gt;add the new partition to the fstab to mount on system startup&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;N.B Assume that we are running everything as the root user&lt;/p&gt;
&lt;h2 id="flatten-the-disk"&gt;Flatten the disk
&lt;/h2&gt;&lt;p&gt;As we cant recover anything we are going to flatten the disk using &lt;code&gt;parted&lt;/code&gt; (&lt;code&gt;apt install parted&lt;/code&gt; to install) to allow is to create a partition greater than 2Tb, but first we are going to identify the disk we are working with&amp;hellip; I tend to favour using either &lt;code&gt;fdisk -l&lt;/code&gt; or as a more concise option &lt;code&gt;lsblk -p&lt;/code&gt; which gives us a an easy to interpret overview something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I can tell from this that we are looking at using the disk that is currently at /dev/sdb and its showing as being 3.7Tb in size.&lt;/p&gt;
&lt;p&gt;Great&amp;hellip; now to set up our new partition using the command &lt;code&gt;parted /dev/sdb&lt;/code&gt; which gives us an interactive shell to work with (you can see the prompts in the output below are prefixed with (parted)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GNU Parted 3.2 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using /dev/sdb 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Welcome to GNU Parted! Type &amp;#39;help&amp;#39; to view a list of commands. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) mklabel gpt 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The command &lt;code&gt;mklabel gpt&lt;/code&gt; will wipe the partition table for &lt;code&gt;/dev/sdb&lt;/code&gt; and give us a clean slate to work from&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) unit TB 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now set parted to think in Terabytes as the default reference size using the command above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) mkpart primary 0.00TB 3.70TB 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we get to create the actual partition. You can see from the command above that we are using the command &lt;code&gt;mkpart&lt;/code&gt; and telling it to create a &lt;code&gt;primary&lt;/code&gt; partition type.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) print 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Model: ATA WDC WD4005FZBX-0 (scsi) 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/sdb: 4.00TB 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512B/4096B 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition Table: gpt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk Flags: 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Number Start End Size File system Name Flags 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1 0.00TB 4.00TB 4.00TB primary 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can chek everything went smoothly using the &lt;code&gt;print&lt;/code&gt; command which gives us confirmation that a new primary partition is present. We can now leave &lt;code&gt;parted&lt;/code&gt; with a simple.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) quit 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we can now use &lt;code&gt;fdisk -l&lt;/code&gt; or &lt;code&gt;lsblk -p&lt;/code&gt; to see that we now have a partition waiting for us at &lt;code&gt;/dev/sdb1&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sdb1 8:17 0 3.7T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="generating-an-encryption-key"&gt;Generating an encryption key
&lt;/h2&gt;&lt;p&gt;Our disk is now ready for use, but not yet encrypted, so our next step is to create a key that can be used when we encrypt the disk. As we are going to be mounting it automatically we want to use a keyfile to store the key. You can of course create a key by mashing the keys on the keyboard, but I tend to prefer letting something else do the hard part for me.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="429px" data-flex-grow="178" height="559" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1.jpg" srcset="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1_hu_5a8dd024f6ad6817.jpg 800w, https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1.jpg 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;First we create somewhere to store the key&amp;hellip; I opted for,&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /etc/crypt/keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But feel free to put it wherever you want just as long as its only accessible by the &lt;code&gt;root&lt;/code&gt; user. Next we generate the keyfile using the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dd bs=512 count=4 if=/dev/urandom of=/etc/crypt/keys/sdb1 iflag=fullblock
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here I am using &lt;code&gt;/dev/urandom&lt;/code&gt; as my randomness generator, but you could use any valid generator of your choice. With this set of parameted &lt;code&gt;dd&lt;/code&gt; will read the stream of &amp;ldquo;randomeness&amp;rdquo; and write 2048 bytes to our keyfile at &lt;code&gt;/etc/crypt/keys/sdb1&lt;/code&gt;. If you want to be a little more complex about teh size and shape of your key then have a look at &lt;a class="link" href="https://man7.org/linux/man-pages/man1/dd.1.html" target="_blank" rel="noopener"
 &gt;https://man7.org/linux/man-pages/man1/dd.1.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="encrypting-the-disk"&gt;Encrypting the Disk
&lt;/h2&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="707px" data-flex-grow="294" height="112" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/luks-logo-cropped.png" width="330"&gt;&lt;/p&gt;
&lt;p&gt;Hopefully it will already be installed because you encrypted your root disk at installation, but if not you can run &lt;code&gt;apt install cryptsetup&lt;/code&gt; to get going.&lt;/p&gt;
&lt;p&gt;The command to do the encryption is actually very simple.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup luksFormat /dev/sdb1 /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can see that using the &lt;code&gt;cryptsetup&lt;/code&gt; tool we are asking it to execute teh command &lt;code&gt;luksFormat&lt;/code&gt; but while it says format in the command this is a little misleading as it doesn&amp;rsquo;t actually format the disk but just rewrites a portion of bytes at the beginning of the partition to enable encryption. we then tell it the partition we want encrypting, here its &lt;code&gt;/dev/sdb1&lt;/code&gt; and finally we pass in the keyfile we just generated and saved at &lt;code&gt;/etc/crypt/keys/sdb1&lt;/code&gt;. If you omit the keyfile it will still encrypt teh disk but will prompt you to enter the key manually.&lt;/p&gt;
&lt;p&gt;As soon as you press enter you will be warned of teh danager of what you are doing&amp;hellip; so double check you are encrypting the right partition and follow the instructions that should look something like :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WARNING! 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;======== 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This will overwrite data on /dev/sdb1 irrevocably. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Are you sure? (Type uppercase yes): YES 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command successful. 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s it&amp;hellip; the disk is encrypted and ready to use. There are a few ways you can now work with the disk. the quickest and easiest is to just decrypt the disk manually using cryptsetup to &lt;code&gt;open&lt;/code&gt; the disk.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup open /dev/sdb1 sdb1_crypt -d /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we open &lt;code&gt;/dev/sdb1&lt;/code&gt; and give is a new name of sdb1_crypt and we unlock it using the &lt;code&gt;-d&lt;/code&gt; argument to tell it the keyfile we generated before.&lt;/p&gt;
&lt;p&gt;That is the dis decrypted and ready to roll&amp;hellip; you can now use &lt;code&gt;fdisk -l&lt;/code&gt; or &lt;code&gt;lsblk -p&lt;/code&gt; to confirm that it is now available at &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sdb1 8:17 0 3.7T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sdb1_crypt 253:3 0 3.7T 0 crypt /mnt/4tb-1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells us that the newly decrypted disk is now available at &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt; and is a volume of 3.7Tb&amp;hellip; Exactly what we were hoping for!&lt;/p&gt;
&lt;p&gt;All finished with your encrypted disk&amp;hellip; you can just as easily close it again using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup close sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="setting-up-the-filesystem"&gt;Setting up the Filesystem
&lt;/h2&gt;&lt;p&gt;Ok, we have an encrypted partition, we can decrypt it but we cant mount it yet as we don&amp;rsquo;t have a file system to work with. Let&amp;rsquo;s take care of that real quick by opening up the partition again.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup open /dev/sdb1 sdb1_crypt -d /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now that its available we are going to set up an ext4 filesystem using the command &lt;code&gt;mkfs.ext4 /dev/mapper/sdb1_crypt&lt;/code&gt; which, all going according to plan, should look something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mke2fs 1.44.1 (24-Mar-2018) 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating filesystem with 976753664 4k blocks and 244195328 inodes 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem UUID: d797be67-c53e-49d3-897e-c624b21a22d3 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Superblock backups stored on blocks: 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 102400000, 214990848, 512000000, 550731776, 644972544 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Allocating group tables: done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Writing inode tables: done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating journal (262144 blocks): done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Writing superblocks and filesystem accounting information: done
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;we are now good to go&amp;hellip; lets try mounting the filesystem with&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -t ext4 /dev/mapper/sdb1_crypt /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m just mounting straight to &lt;code&gt;/mnt&lt;/code&gt; but obviously this can be any folder you want. If the command worked we can easily confirm it with a quick &lt;code&gt;df&lt;/code&gt; -h:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem 1K-blocks Used Available Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;udev 32846708 0 32846708 0% /dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 6578140 2308 6575832 1% /run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/ubuntu--vg-root 1919562064 993193376 828790500 55% /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 32890688 200 32890488 1% /dev/shm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 5120 4 5116 1% /run/lock
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 32890688 0 32890688 0% /sys/fs/cgroup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda2 721392 276068 392860 42% /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 523248 6232 517016 2% /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/sdb1_crypt 3844637680 0 3844637680 1% /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Excellent&amp;hellip; you can now start working with your new partition&amp;hellip; however lets un-mount and close the drive quickly with a&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;umount /mnt &amp;amp;&amp;amp; cryptsetup close sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then we can move onto&amp;hellip;&lt;/p&gt;
&lt;h2 id="automatic-decryption"&gt;Automatic Decryption
&lt;/h2&gt;&lt;p&gt;This is a lot simpler that you may realise&amp;hellip; all we need to do is add a new line to the file &lt;code&gt;/etc/crypttab&lt;/code&gt;! But first we need one last piece of information we don&amp;rsquo;t yet have, but we can easily get with the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cryptsetup luksDump /dev/sdb1 | grep &amp;#34;UUID&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will use &lt;code&gt;luksDump&lt;/code&gt; to get information about the encrypted partition and then uses &lt;code&gt;grep&lt;/code&gt; to specifically target the property UUID which we will need to identify the partition in the next step.&lt;/p&gt;
&lt;p&gt;Now in your favourite editor of choice add the following line, replacing the spoof UUID here with the one we just found.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sdb1_crypt UUID=1111111111-2222-3333-4444-555555555555 /etc/crypt/keys/sdb1 luks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we are giving the decrypted volume a unique label for teh decrypted label to be made available at the appropriate &lt;code&gt;/dev/mapper/*&lt;/code&gt; location. We also specify the UUID to identify the partition to decrypt&amp;hellip; we could use the path &lt;code&gt;/dev/sdb1&lt;/code&gt; but using the UUID is more explicit and prevents any confusion if another partition happens to present itself as &lt;code&gt;/dev/sdb1&lt;/code&gt; at some point in the future. Third we have the path to our newly generated keyfile and finally we have the encryption mode that we are using for encryption which here is &lt;code&gt;luks&lt;/code&gt;. For more info on crypttab have a look at &lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/crypttab.html" target="_blank" rel="noopener"
 &gt;https://www.freedesktop.org/software/systemd/man/crypttab.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can now test that auto decryption is working using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptdisks_start sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which if successful should have an output like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * Starting crypto disk... 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt: INSECURE MODE FOR /etc/crypt/keys/sdb1, see /usr/share/doc/cryptsetup/README.Debian. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt (starting).. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt (started)... 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All that&amp;rsquo;s left to do now is set up&lt;/p&gt;
&lt;h2 id="auto-mount-the-filesystem"&gt;Auto-mount the filesystem
&lt;/h2&gt;&lt;p&gt;Hopefully we now are on really familiar ground&amp;hellip; we can now treat &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt; as a bog standard ext4 partition that can be mounted via the &lt;code&gt;/etc/fstab&lt;/code&gt; by adding the line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/sdb1_crypt /mnt ext4 defaults 0 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see its pretty ordinary, exactly as you would expect, obviously swapping out &lt;code&gt;/mnt&lt;/code&gt; with the location of your choice to mount the filesystem. If you are not wholly familiar with &lt;code&gt;fstab&lt;/code&gt; then its definitely worth having a look at &lt;a class="link" href="https://help.ubuntu.com/community/Fstab" target="_blank" rel="noopener"
 &gt;https://help.ubuntu.com/community/Fstab&lt;/a&gt; as it gives a good overview for those who are new to it&amp;hellip;&lt;/p&gt;
&lt;p&gt;Finally we can check that it all works with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And at this point I am pretty sure I can hear the fat lady singing&amp;hellip;&lt;/p&gt;</description></item><item><title>Connecting to Ubuntu 18.04+ using RDP</title><link>https://blog-570662.gitlab.io/connecting-to-ubuntu-18-04-using-rdp/</link><pubDate>Mon, 20 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/connecting-to-ubuntu-18-04-using-rdp/</guid><description>&lt;p&gt;We have a mix of different setups that the Software Engineer and Data Scientists use to get their work done. There are some using just Linux on laptops, Some on MacBooks and some on the various versions of Windows.&lt;/p&gt;
&lt;p&gt;For those not using Linux as their primary OS we have a bunch of Desktops that run Ubuntu 18.04+ for them to connect to. SSH can do quite a lot but a few of the team work remotely and in house we prefer RDP for that kind of thing rather than VNC.&lt;/p&gt;
&lt;p&gt;We have had some issues with connections in the past so this post exists to remind me how next time I need to set it up. First we need to install the xRDP server package.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install xrdp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next we need to ensure that we have the right ports open on the workstation. If like me you also use UFW to manage your firewall rules then open port 3389 using&amp;hellip;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow &lt;span class="m"&gt;3389&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The issue left, is that you will get an annoying pop up when you log in about a colour management profile needing to be set up and asking you to provide your password. Even then you may still get some annoying crash pop-ups.&lt;/p&gt;
&lt;p&gt;I found a really good solution to this at &lt;a class="link" href="http://c-nergy.be/blog/?p=12043" target="_blank" rel="noopener"
 &gt;http://c-nergy.be/blog/?p=12043&lt;/a&gt; which I&amp;rsquo;ve cribbed and paraphrased below&lt;/p&gt;
&lt;p&gt;Create the file &lt;code&gt;/etc/polkit-1/localauthority/50-local.d/45-allow-colord.pkla&lt;/code&gt; (using your editor of choice and &lt;code&gt;sudo&lt;/code&gt;)and add the following contents&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Allow Colord all Users]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Identity=unix-user:*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultAny=no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultInactive=no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultActive=yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now need to clear any crash dumps from the workstation&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sudo&lt;/span&gt; &lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;crash&lt;/span&gt;&lt;span class="o"&gt;/*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should then be good to connect to using whatever RDP client you prefer&amp;hellip; I like &lt;a class="link" href="https://remmina.org/" target="_blank" rel="noopener"
 &gt;Remmina&lt;/a&gt; myself but each to their own.&lt;/p&gt;</description></item><item><title>Technical CV writing is hard</title><link>https://blog-570662.gitlab.io/technical-cv-writing/</link><pubDate>Wed, 15 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/technical-cv-writing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/technical-cv-writing/writing-a-cv.jpg" alt="Featured image of post Technical CV writing is hard" /&gt;&lt;p&gt;Recruiting people is effing hard!&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all&amp;hellip; I&amp;rsquo;ll get back to reading through CVs now and let you get on with your day!&lt;/p&gt;
&lt;p&gt;Medicines Discovery Catapult is, at the time of writing, recruiting Software Engineers, and as &amp;ldquo;Head of&amp;rdquo; it falls to me to start filtering through the CVs that land in my inbox. But what a mess! I get all sorts, from 1-page masterpieces that look amazing and all glossy, but tell me nothing about the individual to 10 pages of war and peace that have so much in it spanning 20 years of experience that 70% is actually completely irrelevant to the job they have applied for.&lt;/p&gt;
&lt;p&gt;So now I get to say that this is what the perfect portrait of a CV should look like&amp;hellip; I&amp;rsquo;m sorry to disappoint but I don&amp;rsquo;t think there is such a thing as a &amp;ldquo;perfect&amp;rdquo; CV. It&amp;rsquo;s far too subjective and open to interpretation. Instead, I&amp;rsquo;m gonna rip apart my own CV and explain why I chose to write it like I have and justify reasons why I would expect to see similar things on the CVs I am forced to read.&lt;/p&gt;
&lt;p&gt;If you want the &amp;ldquo;TL;DR&amp;rdquo; version you can find a copy of my CV at &lt;a class="link" href="https://docs.google.com/document/d/1MM_6nXIVU_wbrvkhdGL5xaJEwc46L7JblUqUot_g1ec" target="_blank" rel="noopener"
 &gt;https://docs.google.com/document/d/1MM_6nXIVU_wbrvkhdGL5xaJEwc46L7JblUqUot_g1ec&lt;/a&gt; but if you have gotten this far you may as well stick around and read the rest. I promise it won&amp;rsquo;t take long.&lt;/p&gt;
&lt;h2 id="the-basics"&gt;The Basics
&lt;/h2&gt;&lt;p&gt;OK! let&amp;rsquo;s start with some general observations I follow with regard to my own CV. Then we can get into the nitty-gritty of how bad mine is!&lt;/p&gt;
&lt;h3 id="recruiters"&gt;Recruiters
&lt;/h3&gt;&lt;p&gt;Love them or loathe them they are a part of the recruitment ecosystem and they will help you get hired. When you send them a copy of your CV bear in mind that they will most likely try to squeeze it into a format that will include their own branding and a cover sheet with some pertinent details on it. They may also then start redacting information off your CV in an effort to anonymise you. It is also possible that they will use the same CV for multiple roles meaning you will need to insist that they send your tailored CV if you are really bothered about winning a specific position.&lt;/p&gt;
&lt;p&gt;Make sure to discuss this with your recruiter before having them put you forward for a role you are keen on.&lt;/p&gt;
&lt;h3 id="keeping-up-to-date"&gt;Keeping Up to Date
&lt;/h3&gt;&lt;p&gt;Design your CV so that it is easy to tweak and update! Keeping your CV up to date is essential, and I don&amp;rsquo;t mean just adding your last role when you finally get fed up with your current employer abusing your good nature and rage quit!&lt;/p&gt;
&lt;p&gt;Keeping your CV in top form means that you need to review the &lt;strong&gt;entire&lt;/strong&gt; thing whenever you make an update. As your career progresses you will find that your perspective will change on what previous roles consisted of and how they would impact/reflect on the position you now desire.&lt;/p&gt;
&lt;p&gt;You will also find that an up-to-date CV is easier to tailor to any specific job you might be applying for&lt;/p&gt;
&lt;h3 id="prettiness"&gt;Prettiness
&lt;/h3&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="154px" data-flex-grow="64" height="300" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-05-15-09-34-26-193x300.png" width="193"&gt;&lt;/p&gt;
&lt;p&gt;I know his name is Riccardo, but I&amp;rsquo;m struggling to focus on anything else&lt;/p&gt;
&lt;p&gt;Having a pretty CV is great! and if creativity is relevant to the vacancy you are hoping to fill then go for it&amp;hellip; make it gorgeous. It is however very subjective so make sure that you understand what your potential employer is looking for. Sometimes less is more, especially considering they may be scanning through hundreds of CVs. If they have to spend half an hour looking for something specific, then it&amp;rsquo;s gotten lost in all that creativity. If you want to see some gorgeous-looking CVs take a look at &lt;a class="link" href="https://weare.guru/creative-cvs/" target="_blank" rel="noopener"
 &gt;https://weare.guru/creative-cvs/&lt;/a&gt; all of them are creative and beautiful&amp;hellip;&lt;/p&gt;
&lt;p&gt;But&amp;hellip; they could take 20 minutes, or more, for someone to read and extract the right information. Which will waste a lot of time for your potential new boss. Plus we are talking about technical CVs and I am not the most creative of individuals, so keeping it clean and well formatted with consistent fonts usage and sizing&lt;/p&gt;
&lt;h3 id="length"&gt;Length
&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve heard a lot of people say different things about how long your CV should be. 1 side of A4&amp;hellip; 2 sides of A4 but only if it&amp;rsquo;s printed double-sided, even &amp;ldquo;length doesn&amp;rsquo;t matter&amp;rdquo; because the more information you put in the better.&lt;/p&gt;
&lt;p&gt;I would recommend taking a middle-of-the-road approach. Keeping things concise is paramount&amp;hellip; but if you need 3 or 4 pages then that&amp;rsquo;s OK&amp;hellip; as long as you make the content captivating and interesting for the reader, that is what matters&lt;/p&gt;
&lt;h3 id="pdf"&gt;PDF
&lt;/h3&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="150" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/adobe-pdf-icon-logo-png-transparent-150x150.png" width="150"&gt;&lt;/p&gt;
&lt;p&gt;This one feels like it should be a &amp;ldquo;no brainier&amp;rdquo;&amp;hellip; Make sure to submit your CV as a PDF!&lt;/p&gt;
&lt;p&gt;There are two very good reasons behind this. First, it ensures that the reader will view it exactly as you intended. If you send it over as a Word, Google, OpenOffice or other such documents, then you are not guaranteed the reader will be using the exact same tools. I for one don&amp;rsquo;t use Word and an awful lot of presentation is lost because someone used a fancy word feature or have a font that I can&amp;rsquo;t get hold of.&lt;/p&gt;
&lt;p&gt;Second, While it is not impossible it discourages recruiters from tampering with it and spoiling all your hard work. I have known only a few recruiters in my time that are willing to learn (or pay for) a good PDF editing suite. It is possible for them to alter things, but they tend to have to jump through hoops.&lt;/p&gt;
&lt;h2 id="beginnings"&gt;Beginnings
&lt;/h2&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="1055px" data-flex-grow="439" height="188" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.intro_.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.intro__hu_6f720dcded107b1.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.intro_.png 827w" width="827"&gt;&lt;/p&gt;
&lt;p&gt;And so we have the start of My CV. Looks pretty boring, doesn&amp;rsquo;t it? Black text on a white background. Nothing fancy!&lt;/p&gt;
&lt;p&gt;We have some basic contact details that can be used to contact me and a very short profile statement. That is all I feel an employer needs to see before we get into the next section of my CV. On the surface, it doesn&amp;rsquo;t actually say much but lets scratch a little deeper.&lt;/p&gt;
&lt;h4 id="font"&gt;Font
&lt;/h4&gt;&lt;p&gt;I have specifically chosen a font that I think looks clean and professional, With a nice easy typeface, it becomes easy for an employer to scan the CV. I keep my CV in Google Docs so I went with Raleway, which I think is nice, clean, professional and easy to read.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also chosen consistent font sizes and spacing;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;16 for the main title&lt;/li&gt;
&lt;li&gt;14 for headings&lt;/li&gt;
&lt;li&gt;12 for subheadings&lt;/li&gt;
&lt;li&gt;11 for everything else&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="limiting-liability"&gt;Limiting Liability
&lt;/h4&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="1000" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000.jpg" srcset="https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000_hu_3287dab37d2965d3.jpg 800w, https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000.jpg 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;Middle-aged, liberal, heterosexual white male programmer Swipe right to Hire Me!!! Please!&lt;/p&gt;
&lt;p&gt;It may sound odd but I put as little personal information in my CV as possible. Hence why just my name and contact details exist. I don&amp;rsquo;t mention my age, gender, race, driver status or political preferences. People come with inherent biases, that is just a fact of life. so putting as little as possible negates triggering these biases.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve seen people put all sorts of information into CVs&amp;hellip; a personal bugbear is photos. As they can introduce a massive amount of opinion in the eyes of the beholder. Think Tinder but for recruitment!&lt;/p&gt;
&lt;p&gt;So unless specifically asked for I would strongly recommend keeping it to a minimum and using the space it would consume in selling the things that matter.&lt;/p&gt;
&lt;h4 id="profile-statement"&gt;Profile statement
&lt;/h4&gt;&lt;p&gt;1 paragraph! that&amp;rsquo;s all I needed to say regarding what is effectively a personal statement. This is not a &lt;a class="link" href="https://www.ucas.com/" target="_blank" rel="noopener"
 &gt;UCAS&lt;/a&gt; application and I don&amp;rsquo;t need to detail a million things about myself. What is to come next will be the real sales pitch&lt;/p&gt;
&lt;p&gt;My profile statement focuses specifically on what my future employer is going to get from me if they hire me. Passion! It is a statement of intent, specifically written to demonstrate that I will strive to bring my &amp;ldquo;A game&amp;rdquo; to anything I do in the future and also that I intend to encourage those around me to do the same. In effect, it is the opening line to what is a sales pitch.&lt;/p&gt;
&lt;p&gt;I could expand on this to state other goals and ambitions but that would just detract from the main objective of the CV. Plus we will get an opportunity to elaborate later on in the recruitment process.&lt;/p&gt;
&lt;h2 id="skills"&gt;Skills
&lt;/h2&gt;&lt;p&gt;Now we come to the meat and bones of the CV. This is the headline! the bit that you will tailor the most in order to impress whoever is scheduling those interviews and guarantee you a chance to shine. If you can make this captivating enough for the person you need to impress they will then be more than happy to read the rest&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="251px" data-flex-grow="104" height="792" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.skill_.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.skill__hu_d839434784c23aee.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.skill_.png 831w" width="831"&gt;&lt;/p&gt;
&lt;p&gt;My CV has a LOT of skills listed. This is mainly because I&amp;rsquo;m a show off more than anything else and in reality, I will tailor this heavily to suit whatever role I&amp;rsquo;m applying for. For example, if it doesn&amp;rsquo;t have a requirement for managerial skills I would drop that section entirely. Depending on the required attributes and skills asked for I would happily add/remove bits to any section of my skills.&lt;/p&gt;
&lt;p&gt;With different types of skills, I will define them differently. As you can see I am quite generic with Managerial skills. A lot of people understand these and are looking for confidence in your ability to do certain types of tasks. Technical skills define different aspects of what I do that are not specifically tied to languages. It can be very difficult to be curt and to the point here. I favour labelling a technology area and then listing a few of the most prominent or recent items from my repertoire. A bad example on my CV is databases&amp;hellip; I can use quite a few as you can see, but in reality, I&amp;rsquo;ve been greedy by listing MySQL, MariaDB &amp;amp; Percona&amp;hellip; they are all effectively connected to each other, but my need to show off is far too much for me to resist.&lt;/p&gt;
&lt;h4 id="experience"&gt;Experience
&lt;/h4&gt;&lt;p&gt;When I first started out as a Dev I had a hiring manager who once told me that most of the time the main thing he wanted to see was;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;did the applicant have the skills he wanted&lt;/li&gt;
&lt;li&gt;what was the candidates&amp;rsquo; personal assessment of their ability&lt;/li&gt;
&lt;li&gt;how much genuine commercial experience do they have&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three really simple things that stuck with me. So the next time I came to write my CV I added a table for Skills which had only a few things on it back then but has grown massively over the years. I&amp;rsquo;ve tweaked and tailored it over the years, adding and removing things that were relevant as needed.&lt;/p&gt;
&lt;p&gt;By quite clearly labelling my own perception of my skills, I give the person reading it an indication of my potential value. It allows them to tailor the interviews and technical tests to fit me as an individual. It also acts as a double-edged sword as the higher I grade myself the more chance of falling flat on my face when I get asked a question I can&amp;rsquo;t answer&amp;hellip; So it&amp;rsquo;s always better to try and be accurate, but also not to be timid, there are always other competitors in the recruitment race.&lt;/p&gt;
&lt;p&gt;I favour using really simple words to identify my skill level; beginner, intermediate, expert and expert+. It makes it simple for the reader to gauge and also allows me to mix the terms around&lt;/p&gt;
&lt;p&gt;Stating how many years of commercial experience you have lets the reader understand your commercial experience. The number of times I&amp;rsquo;ve had recruiters do a typical keyword match and see that I mention c# once on my CV and then try to wave a job advert under my nose. I did .NET for 6 months 10 odd years ago. I legitimately can claim that experience, but it should not be the core on which I base my next adventure.&lt;/p&gt;
&lt;p&gt;As a useful side effect, by stating a duration in years prompts me to review and update my CV regularly to keep it up to date.&lt;/p&gt;
&lt;h2 id="previous-employment"&gt;Previous Employment
&lt;/h2&gt;&lt;p&gt;By this point, I&amp;rsquo;m hoping that I&amp;rsquo;ve captured the readers&amp;rsquo; imagination. That they now envision a Development God has graced them with a CV worthy of filling any role they have&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="354px" data-flex-grow="147" height="670" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34_hu_b129b9df07d8b244.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34.png 990w" width="990"&gt;&lt;/p&gt;
&lt;p&gt;And then come back down to earth with a bump! Next up in the firing line is Work Experience. Here I define some of the previous positions I have held in my illustrious career in somewhat chronological order. I say &lt;strong&gt;some&lt;/strong&gt; because I have a general rule of limiting what I put on the CV to either 10 years or 10 positions, whichever comes first. This hasn&amp;rsquo;t been a hard and fast rule over the years, and it&amp;rsquo;s fine to flex in order to suit the roles I&amp;rsquo;ve gone for. There is no reason whatsoever though to go so far back in time as to describe my time as a pot-washer when I was 17.&lt;/p&gt;
&lt;p&gt;In some cases, I&amp;rsquo;ve also omitted some things from my CV. Somewhere in and around 2012 - 2015; I happened to found and run a co-working space in Manchester City Centre. It was an interesting venture that I&amp;rsquo;m really proud of and am glad to say is still running even though I&amp;rsquo;m no longer a part of it. But in truth, it does not add anything of value to my CV for the positions I plan to go for in the future.&lt;/p&gt;
&lt;h4 id="to-the-point"&gt;To the point
&lt;/h4&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="360px" data-flex-grow="150" height="200" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/bigstock-From-Point-A-To-Point-B-41405269-300x200.jpg" width="300"&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t like big sprawling paragraphs of text if you haven&amp;rsquo;t already got the gist. So for me, bullet points are the way forward. I try to keep it fairly obvious and detail achievements and document things I have done, and not the tools I have used.&lt;/p&gt;
&lt;p&gt;I try to explain why I joined the company and what my purpose was. I highlight important successes and demonstrate improvements I made to the company. Short succinct sentences should be chosen to illustrate aspects of your accomplishments that actually relate to your future goals in your new position.&lt;/p&gt;
&lt;p&gt;By doing it this way you also make it easier to tweak and change things without having to restructure a whole piece of prose in the future.&lt;/p&gt;
&lt;p&gt;That said my own CV has a glaring exception! When I was doing freelance work I was not able to accurately describe everything I was doing due to some pesky Non Disclosure Agreements. So instead I have a simple paragraph providing a positive high-level explanation of what benefits I brought to my clients.&lt;/p&gt;
&lt;h4 id="formatting"&gt;Formatting
&lt;/h4&gt;&lt;p&gt;There is a lot of information condensed into this section which makes could make it hard to read if left unformatted. I stuck to the font sizes I had chosen previously and decided to use a more subtle combination of indentation, italics, underline and bold to make it more pleasing to the eye and easier to scan.&lt;/p&gt;
&lt;p&gt;Each company name acts as a subheading with a font size of 12. I take a little liberty and include on the same line the dates that I was with them. This gives a clear timeline of events that a manager can then refer to quickly when they need it, such as in an interview.&lt;/p&gt;
&lt;p&gt;Indenting everything under a subheading makes it easier for the reader to separate out the content easily. I include an address for the company. I am not actually sure why if I&amp;rsquo;m honest, it&amp;rsquo;s just something I&amp;rsquo;ve always done. I put this in Italics mainly because it&amp;rsquo;s an aside to the core of the information.&lt;/p&gt;
&lt;p&gt;The job title will come next in bold of course to help it stand out. Followed immediately by the relevant bullet points. My last role obviously plays the most prominent part as it&amp;rsquo;s my headliner. In the case of Medicines Discovery Catapult. I&amp;rsquo;ve held two roles, &amp;ldquo;DevOps Engineer&amp;rdquo; and &amp;ldquo;Head of Software Engineering&amp;rdquo; so I break these into their own sections within this piece of experience. Prior to that, I was with &lt;a class="link" href="https://wakelet.com/" target="_blank" rel="noopener"
 &gt;Wakelet&lt;/a&gt; and here I merge the two roles I held there in order to conserve space.&lt;/p&gt;
&lt;h2 id="education"&gt;Education
&lt;/h2&gt;&lt;p&gt;The bulk of the hard work is now done! We have put in the sales pitch and hopefully, we are close to being invited to an interview. Time to put in some supporting information&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="704px" data-flex-grow="293" height="281" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.education.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.education_hu_b537610f276f7de3.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.education.png 825w" width="825"&gt;&lt;/p&gt;
&lt;p&gt;This may come as a surprise to some people but I didn&amp;rsquo;t do the whole University thing&amp;hellip; I mean I lived in a University city and frequented the student union bars, but was never actually enrolled in a course. To this effect, I bolstered my CV when I started out by going and obtaining some (now significantly outdated) professional certifications.&lt;/p&gt;
&lt;p&gt;Regardless of my lack of educational achievements I would always recommend keeping it simple unless it is your first position and you have no work experience (writing a CV in that situation probably needs to be another blog post entirely). List them in chronological order with some dates and a summary of what you obtained&amp;hellip; no one needs to know that I completely fluked getting a GCSE in Art.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="169" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/educ-300x169.jpg" width="300"&gt;&lt;/p&gt;
&lt;p&gt;Were I to have a Degree I would obviously have the institution and dates in there along with my final marks. I would also look at listing the modules I completed that are relevant to my career, providing I did a Computer Sciences degree and not something like Biology. Again, it&amp;rsquo;s all about keeping to the point and providing specific information to bolster everything else you may have done.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping up
&lt;/h2&gt;&lt;p&gt;Time to finish off with a little bit of something personal. I don&amp;rsquo;t want to spend too much time here but I want to show that there is more to who I am than just work. I decided to keep it simple, a simple list of things that I enjoy doing in my spare time (When I have any, being a father of 3). The idea here is that these can become conversation pieces with the people that may be interviewing you. I&amp;rsquo;ve ended up a number of times having interviews where I talk about Scouts.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="904px" data-flex-grow="377" height="222" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal_hu_1a75be4cf4f02610.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal.png 837w" width="837"&gt;&lt;/p&gt;
&lt;p&gt;The final flourish here should be something simple but gives someone a helping hand at learning more about you if they feel so inclined. I include a link to my blog and my GitHub account, but you could include anything that may be relevant.&lt;/p&gt;
&lt;p&gt;Last but not least&amp;hellip; References are always available on request. My referees are varied and have changed over time. Adding them to your CV doesn&amp;rsquo;t actually impart any other information that could get you hired. Name-dropping is &lt;strong&gt;not&lt;/strong&gt; the right way to get a job.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;OK! so it&amp;rsquo;s not a pretty CV by any stretch of the imagination. It&amp;rsquo;s a bit on the long side in its full un-tailored, raw form, though not as long as it could be if I wasn&amp;rsquo;t being diligent in how I want to present myself. But this is the format I&amp;rsquo;ve used as my CV for at least 15 years now and I would say that I&amp;rsquo;ve been really successful in getting interviews out of it. I would say I get at least an 80% success rate of conversions from seeing my CV to a first-stage interview (watch me now jinx myself for the future).&lt;/p&gt;
&lt;p&gt;A CV will never get you the job! It&amp;rsquo;s all down to you excelling in an interview situation and proving how awesome you are and that you can do everything you say you can on your CV. All it is meant to do is get your foot in the door. Hopefully, this breakdown of my CV will help you to take a look at your own CV and work on ways to improve your chances.&lt;/p&gt;</description></item><item><title>Dell DisplayLink D6000 &amp; Ubuntu 18.04+ Issues</title><link>https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/</link><pubDate>Tue, 14 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/</guid><description>&lt;img src="https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/20190514_124153.png" alt="Featured image of post Dell DisplayLink D6000 &amp; Ubuntu 18.04+ Issues" /&gt;&lt;p&gt;I love Ubuntu&amp;hellip; I&amp;rsquo;m pretty fond of dell kit too!&lt;/p&gt;
&lt;p&gt;So I was rather chuffed when I started working at &lt;a class="link" href="https://md.catapult.org.uk" target="_blank" rel="noopener"
 &gt;Medicines Discovery Catapult&lt;/a&gt; because they let me have both. When you look at my desk it looks like it could be an advert for Dell. Laptop, monitors, dock, keyboard and mouse&amp;hellip;. its great when you have a corporate account with a Dell reseller&lt;/p&gt;
&lt;p&gt;However while I&amp;rsquo;ve had a lot of success with the D3000 DisplayLink dock on Ubuntu I found that I&amp;rsquo;m now having to deal with the upgraded D6000&amp;hellip; which doesn&amp;rsquo;t play very nicely with the more recent versions of Ubuntu (we are talking 18.04 and later)&lt;/p&gt;
&lt;p&gt;I kept finding that after a random amount of time the D6000 would randomly seem to power down&amp;hellip; I would lose the screens, audio, networking and USB. and the only way I could fix it is to unplug it from teh laptop and plug it back in. Not ideal, especially if I&amp;rsquo;m in the middle of a video call or debugging something on the net&lt;/p&gt;
&lt;p&gt;Being the kind of techie I am my first port of call checking my logs&amp;hellip; but I couldn&amp;rsquo;t see anything that would cause this random disconnect. So off to google I went&amp;hellip; eventually I found a lot of information telling me it was part of power management causing things to start powering down&amp;hellip; In this case it implied that it was something trying to suspend USB&amp;hellip; which sounded really plausible!&lt;/p&gt;
&lt;p&gt;So a little more research suggested that I should be using laptop mode tools to disable the ability for USB to be suspended. I gave it a go, though I was dubious as in my mind I shouldn&amp;rsquo;t have needed to install an additional package (albeit a great one for tweaking your power management on a laptop running Linux)&lt;/p&gt;
&lt;p&gt;Alas no joy! And I had too much to do to start debugging in depth and ripping apart other peoples code to figure it out.&lt;/p&gt;
&lt;p&gt;What did I do? you ask. Well, I just put up with it for a few weeks, but gradually it began to grate on my nerves. However there was that one day where it didn&amp;rsquo;t turn off&amp;hellip; and that left me perplexed&amp;hellip; I checked if any updates had been applied in my last &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade&lt;/code&gt; &amp;hellip; nothing&amp;hellip;. it then dawned on me that I had plugged in the headset I used for conference calling into the audio in/out on the dock instead of directly into the laptop.&lt;/p&gt;
&lt;p&gt;Now I had a little more information I was able to deduce (with googles help) that the laptop was actually suspending USB, but that the trigger was actually pulseaudio. At this point it becomes really easy to solve the problem.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution
&lt;/h2&gt;&lt;p&gt;Edit &lt;code&gt;/etc/pulse/default.pa&lt;/code&gt; using your preferred editor (and &lt;code&gt;sudo&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;Find the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;### Automatically suspend sinks/sources that become idle for too long&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;load-module module-suspend-on-idle
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And comment it out and save!&lt;/p&gt;
&lt;p&gt;Lastly, because its run as a user service you need to restart the Pulse Audio daemon using the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user restart pulseaudio.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or you could just logout and back in again&lt;/p&gt;</description></item><item><title>A reboot and a legacy moniker</title><link>https://blog-570662.gitlab.io/a-reboot-and-a-legacy-moniker/</link><pubDate>Tue, 26 Feb 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-reboot-and-a-legacy-moniker/</guid><description>&lt;p&gt;So&amp;hellip; my last post was a good 2 years ago now&amp;hellip;. Hi how have you been?&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a very busy couple of years with a lot of stuff shifting in my personal life meaning things inevitably take a back seat. However its been long enough that I needed to give myself a new start and see about blogging again and getting back into speaking again.&lt;/p&gt;
&lt;h3 id="still-phpboyscout"&gt;Still PHPBoyScout?
&lt;/h3&gt;&lt;p&gt;Since 2016 I&amp;rsquo;ve jumped around in my career a lot! This has exposed me to a lot of new languages and tech, all of which has been awesome but it now means that the moniker of PHPBoyScout probably isn&amp;rsquo;t accurate all that much any more. That said I still have a love for PHP and my first thought when presented with a new challenge is still &amp;ldquo;How would I approach this using PHP?&amp;rdquo;, so I think that means I can keep my twitter handle for a little longer rather than re-brand myself :-D&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve always had a penchant for picking up languages pretty quickly and have been an advocate of the idea that as long as you can think in the right way then languages can be learnt easily enough.&lt;/p&gt;
&lt;h3 id="employed-and-employable"&gt;Employed and Employable
&lt;/h3&gt;&lt;p&gt;October 2017 saw me take on a new role with a company called Medicines Discovery Catapult. They are a grant funded not for profit organisation that is specifically focused on shaking up the medicine discovery pipeline by helping SMEs &amp;amp; CROs innovate and collaborate.&lt;/p&gt;
&lt;p&gt;My role is very broad as I appear to have fallen in to the role of Head of Software Engineering very quickly. Though I started as a DevOps Engineer it has evolved very quickly. We work with a wide array of languages and tools and nothing is off the table if it helps to solve the problems we are working on. Currently we actively have code being written in Python, Node, Go &amp;amp; Scala (I live in hope that I&amp;rsquo;ll be ale to bring PHP into that mix eventually). Vast data sets and AI are the name of the game with my team of engineers helping to support the data scientists and Informaticians.&lt;/p&gt;
&lt;p&gt;At the time of writing we are also hiring so if you are curious take a look at &lt;a class="link" href="https://md.catapult.org.uk/about/careers/" target="_blank" rel="noopener"
 &gt;https://md.catapult.org.uk/about/careers/&lt;/a&gt; and see if you like the sound of what we are doing and the challenge on offer.&lt;/p&gt;
&lt;h3 id="moving-forward"&gt;Moving forward
&lt;/h3&gt;&lt;p&gt;The plan moving forward is to post about the tech we are working with, talk about the types of challenges we are facing and also maybe restart my public speaking career. Stay tuned for more and if you don&amp;rsquo;t hear from me soon then give me a nudge&lt;/p&gt;</description></item><item><title>Project Slayer: The Critical Path</title><link>https://blog-570662.gitlab.io/project-slayer-the-critical-path/</link><pubDate>Tue, 26 Feb 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/project-slayer-the-critical-path/</guid><description>&lt;img src="https://blog-570662.gitlab.io/project-slayer-the-critical-path/c7c3a029d172b33287003d26a0c693f9.png" alt="Featured image of post Project Slayer: The Critical Path" /&gt;&lt;p&gt;The first of my new round of talk abstracts! In all honesty this isn&amp;rsquo;t a talk but something more that came out of a very drunken Saturday night at #phpbnl19. There were a bunch of us sat talking and somehow the topic of D&amp;amp;D came up which sent my mind racing with this idea&amp;hellip; By the time 2am rolled around I had a fully formed idea along with some willing players to help with the idea. Now I just need to find a conference willing to take a chance on it! If you know a conference that would be interested let me know&lt;/p&gt;
&lt;h3 id="descriptionabstract"&gt;&lt;strong&gt;Description/Abstract&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Our heroes have just completed our latest quest! Having successfully delivered the latest iteration of their project they are approached by the aged and holy sage &amp;ldquo;PeeEm&amp;rdquo; with a new quest! How quickly can they implement the sacred and forgotten art of &amp;ldquo;Lo-ging&amp;rdquo; into the codebase&lt;/p&gt;
&lt;p&gt;Do they accept? If they do will they be able to defeat the trials and tribulations that await them? Will they find treasure and glory, or suffer defeat at the hands of the vile and depraved Stakeholder?&lt;/p&gt;
&lt;h3 id="what-will-people-learn"&gt;&lt;strong&gt;What will people learn&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;The only way to find out if our heroes will complete this epic quest will be to join us and see if the dice of fate will be kind to them!&lt;/p&gt;
&lt;p&gt;Along the way we will will learn some truths about feature implementation and how our heroes handle the challenges that lie ahead. And hopefully gain enough experience to level up!&lt;/p&gt;
&lt;h3 id="additional-information"&gt;&lt;strong&gt;Additional Information&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;This is an extremely unique talk! It takes the form of a live Dungeons &amp;amp; Dragons game. I will be on stage playing the part of Dungeon Master looking to guide our team of adventurers through the process of delivering a new feature for a project.&lt;/p&gt;
&lt;p&gt;Our heroes currently consist of a heroic bard who will be inspiring our heroes, and audience, with ballads of past glories. A warlock with the demonic power to fork and merge code like no other in existence&amp;hellip;. Our sorcerer has the innate and wild magic of Fire(base)! Finally our team is held together with the support of our cleric, worshipping the ancient god Rasmus.&lt;/p&gt;
&lt;p&gt;The outcome of this adventure will genuinely be determined by the roll of the dice! It will be a game of 5th edition D&amp;amp;D that we can probably fit into an hour&amp;hellip; but 2 or more would be better and far more fun.&lt;/p&gt;
&lt;p&gt;Audience participation is expected! Cosplay is hoped for! I will, of course, be in full scout uniform!&lt;/p&gt;</description></item><item><title>Monty Python explains why your project failed!</title><link>https://blog-570662.gitlab.io/monty-python-explains-project-failed/</link><pubDate>Sun, 07 Feb 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/monty-python-explains-project-failed/</guid><description>&lt;img src="https://blog-570662.gitlab.io/monty-python-explains-project-failed/9780563558200.jpg" alt="Featured image of post Monty Python explains why your project failed!" /&gt;&lt;p&gt;As part of the attempt to develop my profile as a speaker, I&amp;rsquo;ve realised that I sometimes need to explain a few of my current talk abstracts a bit too much. This is mainly due to my lack of experience writing them and that the majority of my current talk ideas cover large topics that are not as technical as I would like.&lt;/p&gt;
&lt;p&gt;My favourite so far is one title &lt;strong&gt;&amp;ldquo;Python explains why your project failed&amp;rdquo;&lt;/strong&gt;. This is a tongue in cheek talk which aims to poke fun at the Developer, PM and of course the client!&lt;/p&gt;
&lt;h2 id="the-tldr"&gt;The TL;DR
&lt;/h2&gt;&lt;p&gt;This talk has yet to be accepted by anyone&amp;hellip; but will be eventually I hope. In the mean time I wanted to share some of the funny thoughts and comparisons I&amp;rsquo;ve had coming up with the content for the talk. I plan on doing this by writing a series of blog posts one for each topic or sketch that features in the talk.&lt;/p&gt;
&lt;h2 id="the-abstract"&gt;The Abstract
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Python is fantastic! If you haven’t seen it you really need to. Its simple, elegant, powerful and gives you an amazing perspective on what we do as Developers, it is also hilariously funny…. Yes Funny!!!!&lt;/p&gt;
&lt;p&gt;Wait! You thought I was talking about Python the programming language didn&amp;rsquo;t you? I&amp;rsquo;m sorry to tell you but we are actually talking about the most awesome of British comedy acts&amp;hellip; Monty Python.&lt;/p&gt;
&lt;p&gt;Throughout this talk I will take you through the development life-cycle of a project and use the Comedy of Monty Python to illustrate both the Good and the Bad (mainly the bad) aspects of our industry. All the way from Client introduction, requirements gathers, spec writing, team selection, planning and scoping all the way through Development to Testing, Delivery and Support!&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="the-delivery"&gt;The Delivery
&lt;/h2&gt;&lt;p&gt;This is a little harder to explain as I&amp;rsquo;ve not given the talk (yet) and I don&amp;rsquo;t think I could ever match the delivery better than the Pythons themselves.&lt;/p&gt;
&lt;p&gt;However I have been known to dress for the occasion, so it&amp;rsquo;s quite possible that you may find me standing on stage in a red cassock at some point.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also managed to convince &lt;a class="link" href="https://twitter.com/phpcodemonkey" target="_blank" rel="noopener"
 &gt;@phpcodemonkey&lt;/a&gt;, who is as big a Python fan as myself, that this talk should really be performed as a 2 man show, rather than me monologuing at a room full of people.&lt;/p&gt;
&lt;h2 id="the-sketches"&gt;The Sketches
&lt;/h2&gt;&lt;p&gt;Due to the prolific variety of skits and sketches that Pythons created I found it extremely hard to select the few needed to fill a single talk. I have however managed to select a few and will change them around from time to time to suit the audience. I&amp;rsquo;ve provided a short list of a few of my favourites Sketches and a couple of words describing what they explain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spanish inquisition&lt;/strong&gt; - Client Indecision&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead parrot sketch&lt;/strong&gt; - Stubborn Project Managers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ministry of silly &lt;em&gt;(array)&lt;/em&gt; walks&lt;/strong&gt; - Tool Selection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brian&amp;rsquo;s Latin Lesson&lt;/strong&gt; - Planning and Preparation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We demand a shrubbery&lt;/strong&gt; - Demanding the Impossible&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Black knight&lt;/strong&gt; - Solution fixation / Code Blindness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Camelot Song&lt;/strong&gt; - Stakeholder Morale&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The People&amp;rsquo;s Front&lt;/strong&gt; - Team Fragmentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Silly Job Interview&lt;/strong&gt; - Stakeholder Communications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four Yorkshireman&lt;/strong&gt; - Rockstar Developers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Argument clinic&lt;/strong&gt; - Product Delivery&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architects sketch&lt;/strong&gt; - Taking Shortcuts and Cutting Corners&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-finale"&gt;The Finale
&lt;/h2&gt;&lt;p&gt;These are just a few of the potential topics I will be looking to cover in the coming posts, but while your reading them I want you to remember to&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://youtu.be/WlBiLNN1NhQ" target="_blank" rel="noopener"
 &gt;https://youtu.be/WlBiLNN1NhQ&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Are you a Good Code Scout? - NomadPHP lightning talk video</title><link>https://blog-570662.gitlab.io/good-code-scout-nomadphp-lightning-talk-video/</link><pubDate>Wed, 06 Jan 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/good-code-scout-nomadphp-lightning-talk-video/</guid><description>&lt;p&gt;&lt;a class="link" href="https://www.youtube.com/watch?v=Tt0lnauF5lI" target="_blank" rel="noopener"
 &gt;https://www.youtube.com/watch?v=Tt0lnauF5lI&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Just before the Christmas period I was lucky enough to be able to give my &amp;ldquo;Are you a good Code Scout?&amp;rdquo; talk as a lightning talk for NomadPHP. Here is the video that was recorded from it.&lt;/p&gt;</description></item><item><title>Using Gmail aliases with Evolution</title><link>https://blog-570662.gitlab.io/using-gmail-aliases-with-evolution/</link><pubDate>Wed, 06 Jan 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/using-gmail-aliases-with-evolution/</guid><description>&lt;p&gt;If your anything like me you have a large number of email aliases that you use with Gmail which is great. However I use &lt;a class="link" href="https://wiki.gnome.org/Apps/Evolution" target="_blank" rel="noopener"
 &gt;Evolution&lt;/a&gt; as a mail client more often than not when using &lt;a class="link" href="https://www.gnome.org/" target="_blank" rel="noopener"
 &gt;Gnome3&lt;/a&gt; as a desktop.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s very easy to set up Evolution to create separate outbound email accounts that you can use for handling all of your aliases. It doesn&amp;rsquo;t yet support OAuth2 as an authentication mechanism for any account that is not set up using the built-in Gnome Online Accounts integration.&lt;/p&gt;
&lt;p&gt;This is a real pain as Google have disabled the more common &amp;lsquo;plain&amp;rsquo; and &amp;rsquo;login&amp;rsquo; authentication mechanisms for use with an SMTP only account. Meaning that any time that you try to connect to smtp.gmail.com:587 with STARTTLS you will get some form of error message to the effect of &amp;ldquo;Bad Authentication&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Hopefully I&amp;rsquo;ll find a workaround at some point in the near future or Evolution will add the facility to enable OAuth2 as an available authentication mechanism.&lt;/p&gt;
&lt;p&gt;In the mean time there is a workaround if you visit &lt;a class="link" href="https://www.google.com/settings/security/lesssecureapps" target="_blank" rel="noopener"
 &gt;https://www.google.com/settings/security/lesssecureapps&lt;/a&gt; you can enable these less secure authentication mechanisms allowing you to once again connect and send email via email addresses using SMTP&lt;/p&gt;</description></item><item><title>A metaphor about PSR-7 and Middleware for non-developers</title><link>https://blog-570662.gitlab.io/metaphor-psr7-middleware/</link><pubDate>Thu, 08 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/metaphor-psr7-middleware/</guid><description>&lt;img src="https://blog-570662.gitlab.io/metaphor-psr7-middleware/fire-bucket-brigade.jpg" alt="Featured image of post A metaphor about PSR-7 and Middleware for non-developers" /&gt;&lt;p&gt;Never one to shy away from coming up with a metaphor for explaining something technical I found myself having to come up with one on the spot for PSR-7 and Middleware while at the recent PHPNW15 Conference.&lt;/p&gt;
&lt;p&gt;Normally my brain will come up with something completely inappropriate but this time round I found I quite liked the imagery that came to mind.&lt;/p&gt;
&lt;p&gt;If you would like to find out more of the specifics about PSR-7 you can take a look at &lt;a class="link" href="http://www.php-fig.org/psr/psr-7/" target="_blank" rel="noopener"
 &gt;http://www.php-fig.org/psr/psr-7/&lt;/a&gt; which will make a better job of explaining it than I could ever do.&lt;/p&gt;
&lt;p&gt;Now on to the metaphor&lt;/p&gt;
&lt;p&gt;Imagine a house on fire, a bizarre way to start I know but bear with me. The nearest well with water that can put out the fire is 500 meters away! We then have a human chain stretching between the well and the house with a bucket going back and forth between trying to put the fire out. So lets break this down, the house represents the internet, or more specifically you and your browser. The fact you are on fire means that you are desperately needing water to quench the flames. At this point you send an empty bucket which represents your &amp;ldquo;request&amp;rdquo;, along the human chain, which in itself represents the application, to the well.&lt;/p&gt;
&lt;p&gt;At the start of the chain the bucket is pretty normal, it&amp;rsquo;s a bucket of course, its round, made of wood with a rope handle, lets say it has a small leak in it.&lt;/p&gt;
&lt;p&gt;As it travels down the chain it&amp;rsquo;s passed from person to person, everyone in it has the opportunity to do something with the bucket, or not as the case may be and could just pass it to the next person in the chain. Others may attempt to fix the leak in the bucket, someone may choose to replace it with a metal bucket, change the handle or make it bigger. Regardless of what may be done to the bucket in essence it remains a bucket.&lt;/p&gt;
&lt;p&gt;&lt;img alt="colonial_bucket3" class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="500" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/metaphor-psr7-middleware/colonial_bucket3.jpg" width="500"&gt;&lt;/p&gt;
&lt;p&gt;Inexorably the bucket will continue to move down the chain to the well. When it reaches the well it changes state because now it has been filled with water. All of the interaction with the bucket thus far, mean that what happens at the well could vary depending on the changes have been made . If its been made bigger, for example, it could be filled with significantly more water, if swapped for a metal one it could imply that the bucket descends the well to get the water quicker because its heavier. Either way it is filled with water and begins its journey back towards the house.&lt;/p&gt;
&lt;p&gt;Again it passes through the hands of each person in the chain, but now that its state has changed it now has the opportunity to be modified again. Someone may empty some water out as there is too much in the bucket, others may say that there is not enough and send it back down the line towards the well to be refilled. Either way the bucket continues to change hands over and over until it reaches the house and the contents are thrown on the fire to complete the request for water.&lt;/p&gt;
&lt;p&gt;During this whole time the human chain could have been in flux. Some people may have swapped places, left the chain, added to the chain, some extraordinary people may have played leapfrog in the chain and appeared to handle the bucket more than once. Regardless of these changes the chain remains and continues to pass the bucket from one person to the another as long as the requests for water keep coming.&lt;/p&gt;
&lt;p&gt;This, in the simplest possible form, explains PSR-7 and the concept of Middleware.&lt;/p&gt;
&lt;p&gt;The bucket remains a bucket because PSR-7 says that is what is needed to complete the request for water, it also defines how you should interact with it regardless of what modifications have been made. If the bucket cant be used according to how PSR-7 describes a bucket to be, then the middleware can&amp;rsquo;t complete the request.&lt;/p&gt;
&lt;p&gt;Every person in the human chain can be classed as a piece of middleware all the way from the house to the well and back again. If at any point someone enters the chain that doesn&amp;rsquo;t agree that the bucket is a bucket or doesn&amp;rsquo;t know how to handle it, then the it is dropped on the ground and the request fails.&lt;/p&gt;</description></item><item><title>Badges &amp; Stickers</title><link>https://blog-570662.gitlab.io/badges-and-stickers/</link><pubDate>Sun, 04 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/badges-and-stickers/</guid><description>&lt;img src="https://blog-570662.gitlab.io/badges-and-stickers/Screen-Shot-2015-10-04-at-12.03.38.png" alt="Featured image of post Badges &amp; Stickers" /&gt;&lt;p&gt;One of the most prominent things I&amp;rsquo;ve been asked about regarding my promoting being a Good Code Scout, is where can we get the badges?&lt;/p&gt;
&lt;p&gt;Following on from a number of questions and subsequent tweets about it&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://twitter.com/stuherbert/status/650591775732670466" target="_blank" rel="noopener"
 &gt;https://twitter.com/stuherbert/status/650591775732670466&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well&amp;hellip; I&amp;rsquo;ve decided that (providing I can get permission from all the right people) I&amp;rsquo;ll start creating a range of Badges &amp;amp; Stickers for you to earn as a Good Code Scout.&lt;/p&gt;
&lt;p&gt;So if your interested in having some stickers or badges let me know using the form below and if I get enough interest I will most definitely get some made up for you.&lt;/p&gt;
&lt;h2 id="update"&gt;Update
&lt;/h2&gt;&lt;p&gt;Unfortunately I&amp;rsquo;m have no more stickers left, Though I&amp;rsquo;ll be working on diversifying some of the designs in the near future I wont be looking at ordering any more for a little while yet. Keep your eyes on twitter as I&amp;rsquo;ll most likely post there when they are available again.&lt;/p&gt;</description></item><item><title>Wow... What a Conference</title><link>https://blog-570662.gitlab.io/wow-phpnw15-conference/</link><pubDate>Sun, 04 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/wow-phpnw15-conference/</guid><description>&lt;p&gt;So I attended the PHPNW15 conference this weekend and what a weekend. I&amp;rsquo;ve been an attendee of the conference for a number of years and have always enjoyed it immensely. However this year turned out to be something special.&lt;/p&gt;
&lt;p&gt;Following on from my first ever appearance as the PHP Boy Scout I decided to submit to the Unconference at PHPNW15.&lt;/p&gt;
&lt;p&gt;It was a good talk, an extension of the previous lightening talk I&amp;rsquo;d given and felt really good to give. Unbeknownst to me however there was mischief afoot. Normally the Unconference talks are rated by the organisers and the one that they selected as the best gets to have a guaranteed slot in next years PHPNW conference. All of which I had genuinely either no idea about or had forgotten had happened in previous conferences,&lt;/p&gt;
&lt;p&gt;As you may have guessed from the fact this post exists, I ended up winning that slot.&lt;/p&gt;
&lt;p&gt;However&amp;hellip;. it appeared that a Speaker had taken ill at the last minute and couldn&amp;rsquo;t make it at which point I was asked a mere 3 minutes before it was announce that I was also going to be given the hangover slot on track three for the Sunday sessions!!!!!&lt;/p&gt;
&lt;p&gt;Suffice to say I had an interesting evening to say the least, in preparing my talk for &amp;ldquo;the big time&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Amazingly I felt really calm about everything, and even had a good chuckle about managing to find some props to help break the ice!&lt;/p&gt;
&lt;p&gt;Everything is ready, I&amp;rsquo;ve practiced, knowing my talk was going to be a bit short&amp;hellip; but that was ok considering the short notice, and I had plenty of anecdotes I could use as filler. I&amp;rsquo;m sat there waiting for the moment I have to put my head above the parapet and all of a sudden&amp;hellip;&amp;hellip;..&lt;/p&gt;
&lt;p&gt;nothing&lt;/p&gt;
&lt;p&gt;My mind goes blank!&lt;/p&gt;
&lt;p&gt;The long and the short is that I survived, and the feedback I have had has been amazing and I&amp;rsquo;ll be taking all of it on board to make sure that next time its even better!&lt;/p&gt;
&lt;p&gt;The recordings should be available in the near future so when they are I will share a link so you can judge how it went for yourselves. In the mean time I&amp;rsquo;ve published the revised slide deck for you on &lt;a class="link" href="http://slideshare.net/phpboyscout/are-you-a-good-scout-phpnw15-track-3" target="_blank" rel="noopener"
 &gt;slideshare.net/phpboyscout/are-you-a-good-scout-phpnw15-track-3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping that I can now find some opportunities to practice for my slot at #phpnw16&lt;/p&gt;
&lt;p&gt;[slideshare id=53508432&amp;amp;doc=areyouagoodscout-151004073451-lva1-app6892]&lt;/p&gt;</description></item><item><title>What is a PHP Scout</title><link>https://blog-570662.gitlab.io/php-scout/</link><pubDate>Fri, 02 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/php-scout/</guid><description>&lt;p&gt;Recently I&amp;rsquo;ve had a lot of people asking me what a PHP Scout is! I thought it would be a good opportunity to explain.&lt;/p&gt;
&lt;p&gt;To understand what a PHP Scout is it helps to know a little of the background basics of Scouting in general. Knowing this helps to make it easier later on as well as we draw some direct parallels. If you would like to investigate more about the history of Scouting you can find a good starting point at &lt;a class="link" href="http://scouts.org.uk/about-us/history/" target="_blank" rel="noopener"
 &gt;http://scouts.org.uk/about-us/history/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For now I&amp;rsquo;m going to give a tl;dr version;&lt;/p&gt;
&lt;p&gt;Scouting started in 1908 as a movement for training young people to encourage them to develop physically, mentally and spiritually by Robert Baden-Powell. Over the next 100+ years it has evolved to encompass people of all ages, races, colours and creeds to get involved and try to be the best they can be.&lt;/p&gt;
&lt;p&gt;The primary ethos of the movement today is to bring Everyday Adventure to young people and this is achieved through a comprehensive programme scheme that is designed to touch on all aspects of that young persons development. This is then rewarded in a variety of ways with the primary reward being the experience itself, the awarding of badges also strengthens then sense of achievement and desire to work towards the next goal.&lt;/p&gt;
&lt;p&gt;All members of the Scouting movement are required to make and frequently renew a promise:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;On my honour, I promise that I will do my best to do my duty to {insert deity/monarchy here}, to help other people and to keep the Scout Law.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The key part here is &lt;strong&gt;I will do my best&lt;/strong&gt;. Scouts are continually encouraged to improve themselves in everything they do.&lt;/p&gt;
&lt;h2 id="the-boy-scout-rule"&gt;The Boy Scout Rule
&lt;/h2&gt;&lt;p&gt;Lets start with something easy! There is a pretty common piece of guidance that gets bandied about in a lot of different circles that is normally referred to as &amp;ldquo;The Boy Scout Rule&amp;rdquo; which promotes leaving things better than you found it. It came about as common practice for scouts to always try to leave a campsite cleaner and tidier than when they arrived so that its in a good state for the next group.&lt;/p&gt;
&lt;p&gt;This is quite generic but can easily be made very specific to us as programmers:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Leave the codebase better than you found it.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;So what do I mean by this? Ultimately I mean that regardless of the state of the code you are working on you should always try to find a way to improve it.&lt;/p&gt;
&lt;p&gt;This can be something as simple as;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;refactoring the code to make it more readable&lt;/li&gt;
&lt;li&gt;adding some docblock to explain a file/class/method/function/variable&lt;/li&gt;
&lt;li&gt;create a Readme file or add some documentation&lt;/li&gt;
&lt;li&gt;remove obsolete code, old backup files, stray files, unused components&lt;/li&gt;
&lt;li&gt;fix a failing test&lt;/li&gt;
&lt;li&gt;write a new test even&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Its not an exhaustive list at all but it gives you an idea of what kind of things you can be doing to improve your codebase. Any good Scout group leaving a campsite would also make sure to put out the fire and close the gate on your way out. Which is exactly what you should be doing by making sure all your Acceptance, Functional, Integration &amp;amp; Unit tests pass and writing a good commit message.&lt;/p&gt;
&lt;h2 id="right-tool-for-the-job"&gt;Right tool for the job
&lt;/h2&gt;&lt;p&gt;In every activity that a Scout takes part in they are always taught the correct way to work with their tools and equipment, such as how to use an penknife property. They are then encouraged to explore different ways of using those to achieve their goals. This is no different for a PHP Scout, by knowing how to use their languages and tools properly they can then use it to maximum effect.&lt;/p&gt;
&lt;h2 id="self-development"&gt;Self Development
&lt;/h2&gt;&lt;p&gt;As a child you assimilate massive amounts of information every day that helps you to grow and develop. This is creatively harnessed by Scouts through a variety of different activities that are designed to help them learn new skills that can help them grow as people.&lt;/p&gt;
&lt;p&gt;Now that we are older our brains don&amp;rsquo;t have the same capacity to soak up that volume of information. But that doesn&amp;rsquo;t mean we shouldn&amp;rsquo;t be trying! A good PHP Scout will continually strive to push the boundaries of what they know, to pick up new skills that can be used to make them more capable. This can be learning a new technique, or language or tool be it via formal training, conferences, social events or even just a good Google.&lt;/p&gt;
&lt;p&gt;Granted the Scouts are rewarded with some cool badges, but I&amp;rsquo;m sure its only a matter if time before some entrepreneurial PHP Scout decides to start creating some achievement badges of their own (see &lt;a class="link" href="http://phpboyscout.uk/php-scout-membership-badge" target="_blank" rel="noopener"
 &gt;http://phpboyscout.uk/php-scout-membership-badge&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="helping-others"&gt;Helping Others
&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;ve all heard the adage of a Scout helping someone across the street. It&amp;rsquo;s a somewhat stereotypical example but extremely apt as it highlights that they are encouraged to take into consideration other peoples needs and to provide assistance wherever possible. Modern Scouting however goes far beyond aiding with avoiding getting run over on a road.&lt;/p&gt;
&lt;p&gt;By encouraging Scouts to not only help individuals, communities and groups we make them more considerate of the needs of others as well as developing their sense of self. A fantastic example of this is the 2015/16 initiative &lt;a class="link" href="https://www.amillionhands.org.uk/" target="_blank" rel="noopener"
 &gt;A Million Hands&lt;/a&gt; which promotes finding ways to identify the needs of others and to take action to provide aid.&lt;/p&gt;
&lt;p&gt;This is a fantastic trait to be teaching children and is something that any good PHP Scout would applaud, and would then go forward to do the same things but with the development community. This can be something as simple as;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;helping a colleague at work (without being told to do so by your boss)&lt;/li&gt;
&lt;li&gt;organising an event with a local user group&lt;/li&gt;
&lt;li&gt;contributing to an open source project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All very simple stuff to do and all it takes up is a little of your time! Where is the reward? I here some of you ask! I would say that the act itself is its own reward, and in reality that is true as when working helping others your generate some very positive Karma which will eventually be paid back when the day comes that you yourself need some help. You should (hopefully) also have an opportunity to maybe learn something new and improve your ability to communicate, a soft skill yes, but essential to your growth if your are striving to be better than you are now.&lt;/p&gt;
&lt;h2 id="problem-solving"&gt;Problem Solving
&lt;/h2&gt;&lt;p&gt;The Scout motto is a very simple two words&amp;hellip; &amp;ldquo;Be Prepared&amp;rdquo;, but be prepared to do what? Its quite open ended really it could be anything at all! I like to think that its nearly impossible to be equipped with every possible skill and tool possible to meet any and every task you will encounter through life, though being a Scout does try to help arm you with as many as possible.&lt;/p&gt;
&lt;p&gt;Yet as a PHP Scout we should always &amp;ldquo;Be Prepared&amp;rdquo; to solve problems. If we are doing our jobs right we should be looking to solve problems through the solutions we provide every day. Quite often I talk to developers and hear them make pigeon holing statements like&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I am a WordPress developer&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;And then complaining that they are bored at work or that they cant get jobs working with anything other than their chosen platform. Now this infuriates me as a PHP Scout would never do this, when asked they profess loud and clear&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I am a Problem Solver&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;and be prepared to prove it by making sure they &lt;em&gt;are prepared&lt;/em&gt; by knowing more than one or two platforms or frameworks or even programming languages. This can be encouraged by actively seeking &amp;ldquo;problems&amp;rdquo; that you can solve with tools and techniques you are not familiar with.&lt;/p&gt;
&lt;p&gt;This is echoed throughout the challenges that are presented to Scouts, where they are tasked with solving a practical problem such as putting up a tent without any instructions, the best way to light a fire with two sticks and a bit of kindling, how to cross a stream with only a few bits of wood and rope. In solving these types of problems the Scouts not only receive the obvious of shelter, food, heat etc but they also become more prepared for the next time a similar scenario presents itself.&lt;/p&gt;
&lt;h2 id="team-work"&gt;Team work
&lt;/h2&gt;&lt;p&gt;No man is an island as the saying goes and the same goes for being a Scout. By being organised into lodges,packs &amp;amp; patrols they have a ready made team to work with and the only way they can progress is to work together. They may not like the people in their team (and as a Scout Leader I will quite readily admit to putting Scouts into groups with others they may clash with).&lt;/p&gt;
&lt;p&gt;We may all have teams that we work with as part of our Jobs, and a PHP Scout will take the opportunity to work with as many different combinations of teams as possible both in and outside of the workplace. By diversifying the people you have to interact with you develop a broader understanding about the problems you may be trying to solve.&lt;/p&gt;
&lt;p&gt;This can then be expanded upon as mentioned previously by then branching out into the community and working with user groups and opensource projects.&lt;/p&gt;
&lt;h2 id="to-summarise"&gt;To Summarise
&lt;/h2&gt;&lt;p&gt;The ethos behind the Scouting movement is a solid foundation not only for children aged 7-18 but for everyone. By being a PHP Scout you strive to keep improving your ability to create great code, solve problems, work with others and in doing so become a better developer.&lt;/p&gt;
&lt;p&gt;As with all Scouts they are Hard Working, Determined, Ingenious &amp;amp; Tenacious and so is a PHP Scout.&lt;/p&gt;</description></item><item><title>The PHP Scout Membership Badge</title><link>https://blog-570662.gitlab.io/php-scout-membership-badge/</link><pubDate>Tue, 11 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/php-scout-membership-badge/</guid><description>&lt;img src="https://blog-570662.gitlab.io/php-scout-membership-badge/elephpant.png" alt="Featured image of post The PHP Scout Membership Badge" /&gt;&lt;p&gt;As the PHP Boy Scout I&amp;rsquo;m having some badges made and I wanted to introduce the all new PHP Scout Membership Badge.&lt;/p&gt;
&lt;p&gt;This badge shows that you are more than just a PHP Developer but also a good PHP Scout. This means that you have all the qualities it takes to be a PHP Scout and will :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always leave the codebase better than you found it&lt;/li&gt;
&lt;li&gt;help other PHP developers be good Scouts&lt;/li&gt;
&lt;li&gt;get involved with your local User Group &amp;amp; PHP Community&lt;/li&gt;
&lt;li&gt;Contribute to at least one open source project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you would like to find out how to get hold of a PHP Scout Membership Badge then fill out the form below.&lt;/p&gt;
&lt;p&gt;[contact-form &lt;a class="link" href="mailto:to=%27matt@phpboyscout.uk" &gt;to='matt@phpboyscout.uk&lt;/a&gt;&amp;rsquo; subject=&amp;lsquo;Someone wants a Membership Badge&amp;rsquo;][contact-field label=&amp;lsquo;Name&amp;rsquo; type=&amp;lsquo;name&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Email&amp;rsquo; type=&amp;lsquo;email&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will always leave the codebase better than I found it&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will help other PHP developers&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will get involved with my local User Group&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will contribute to an open source project&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][/contact-form]&lt;/p&gt;</description></item><item><title>My first ever public appearance as PHPBoyScout</title><link>https://blog-570662.gitlab.io/public-appearance-phpboyscout/</link><pubDate>Thu, 06 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/public-appearance-phpboyscout/</guid><description>&lt;img src="https://blog-570662.gitlab.io/public-appearance-phpboyscout/20111036639_d7c8ec153d_z.jpg" alt="Featured image of post My first ever public appearance as PHPBoyScout" /&gt;&lt;p&gt;So it&amp;rsquo;s finally happened!&lt;/p&gt;
&lt;p&gt;I stood up in front of a group of developers and gave a lightning talk about how Scouting Principles should be applied to every day development.&lt;/p&gt;
&lt;p&gt;[slideshare id=51344270&amp;amp;doc=areyouagoodscout-150806121954-lva1-app6892]&lt;/p&gt;
&lt;p&gt;The amazing thing is that I didn&amp;rsquo;t get any rotten tomatoes thrown at me! quite the contrary in fact. Even with me doing the talk in full Scout uniform.&lt;/p&gt;
&lt;p&gt;Now to see about finding some more places to speak and actually fleshing out the talk into something that can last a full hour and not just shy of 5 minutes.&lt;/p&gt;</description></item><item><title>Goodbye Dev in Charge</title><link>https://blog-570662.gitlab.io/goodbye-dev-charge/</link><pubDate>Wed, 05 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/goodbye-dev-charge/</guid><description>&lt;p&gt;Over all the time that I&amp;rsquo;ve been a developer I&amp;rsquo;ve had people telling me that I should get in front of an audience and speak. However I&amp;rsquo;ve always suffered from a rather bad case of &amp;lsquo;Imposter Syndrome&amp;rsquo; which meant my automatic response to those kind of statements has always been&amp;hellip; I don&amp;rsquo;t really know enough about any one topic.&lt;/p&gt;
&lt;p&gt;This is very true, I&amp;rsquo;ve spent a lot of my career learning a really broad swathe of technologies and techniques so I can turn my hand to any task that&amp;rsquo;s been presented to me so far. Even so people continue to try convince me that it would be a worthwhile pursuit.&lt;/p&gt;
&lt;p&gt;Now that I work at Magma Digital I find that I&amp;rsquo;m often talking with &lt;a class="link" href="https://twitter.com/phpcodemonkey" target="_blank" rel="noopener"
 &gt;@phpcodemonkey&lt;/a&gt; about all sorts of things and the topic of creating a talk came up while we were enjoying the most excellent &lt;a class="link" href="http://2015.phpsouthcoast.co.uk/" target="_blank" rel="noopener"
 &gt;PHP South Coast Conference&lt;/a&gt;. He knows I&amp;rsquo;ve been a Scout Leader for around 4 years now, and he suggested that I do a talk on the &amp;lsquo;Boy Scout Rule&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know if he was serious or not at the time but it set my mind racing! This is a topic that I actually know quite a lot about!&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m now going to leave behind the Dev in Charge and have now rebranded as the PHP Boy Scout. I&amp;rsquo;ve already managed to pull together the basis of a talk on how Scouting principles can be used in conjunction with what we do as Developers and have a few other ideas that I&amp;rsquo;m going to work on over the next few weeks.&lt;/p&gt;
&lt;p&gt;Fingers crossed I will be better at doing this kind of thing than I suspect I will be&amp;hellip; but nothing ventured and nothing gained!&lt;/p&gt;</description></item><item><title>Free Open Source Website for Scouts</title><link>https://blog-570662.gitlab.io/free-open-source-website-scouts/</link><pubDate>Mon, 12 Jan 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/free-open-source-website-scouts/</guid><description>&lt;img src="https://blog-570662.gitlab.io/free-open-source-website-scouts/scouts-snapshot.png" alt="Featured image of post Free Open Source Website for Scouts" /&gt;&lt;p&gt;I&amp;rsquo;ve been a Scout Leader for a few years now and the District I work within have very little by way of internet presence. As a bit of a pet project I started building a simple Scout based website for them to use.&lt;/p&gt;
&lt;p&gt;Its nothing too fancy, I created a simple module and theme for the Silverstripe CMS and have now put it into a GitHub Repository to share with the wider scouting community.&lt;/p&gt;
&lt;p&gt;I chose Silverstripe because of the speed with which I could develop something usable as well as providing a super simple management interface that can be handled by users of all skill levels.&lt;/p&gt;
&lt;p&gt;The module itself extends some very common extensions available for the CMS and makes them scout focused. Features include.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customisable theme&lt;/li&gt;
&lt;li&gt;Multi tiered Event Calendars&lt;/li&gt;
&lt;li&gt;Customisable Group/Section Pages&lt;/li&gt;
&lt;li&gt;Dynamic Forms&lt;/li&gt;
&lt;li&gt;A reliable News/Blog system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are just a few of the most obvious features and hopefully I will continue to add more.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also offering to help any Scout Groups/Districts/Counties if they are wanting to use these modules and get their sites built and up and running for them free of charge.&lt;/p&gt;
&lt;p&gt;If you want to take a look at the code and have a play yourselves you can find it online at&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/phpboyscout/silverstripe-scouts" target="_blank" rel="noopener"
 &gt;https://github.com/phpboyscout/silverstripe-scouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/phpboyscout/silverstripe-scouts-theme" target="_blank" rel="noopener"
 &gt;https://github.com/phpboyscout/silverstripe-scouts-theme&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to get in touch or would like more information about having a website built for you please fill in the form below.&lt;/p&gt;
&lt;p&gt;[contact-form &lt;a class="link" href="mailto:to=%27matt@phpboyscout.uk" &gt;to='matt@phpboyscout.uk&lt;/a&gt;&amp;rsquo; subject=&amp;lsquo;Request for Scouts Website&amp;rsquo;][contact-field label=&amp;lsquo;Name&amp;rsquo; type=&amp;lsquo;name&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Email&amp;rsquo; type=&amp;lsquo;email&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Scout Group/District/County&amp;rsquo; type=&amp;lsquo;url&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][/contact-form]&lt;/p&gt;</description></item><item><title>Its time for a change</title><link>https://blog-570662.gitlab.io/time-change/</link><pubDate>Tue, 02 Dec 2014 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/time-change/</guid><description>&lt;p&gt;So&amp;hellip; Its been a long time since I posted anything of any relevance. This is due to having been super busy with my previous company Zucchi.&lt;/p&gt;
&lt;p&gt;However that has all changed now! After three and a half years of running my own company I have decided that its not for me. I gave it my all, but in the end I was becoming too much of a Salesman and I missed getting stuck in with code.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve now moved on and have joined the fantastic team at Magma Digital who have been leaders in PHP software development for somewhere in the region of 14 years as well as heavily involved in the PHP community having been a essential part of the PHPNW user group and conference.&lt;/p&gt;
&lt;p&gt;This means I should be able to pick up where I left off all those years ago and start being more active again.&lt;/p&gt;
&lt;p&gt;See you soon&lt;/p&gt;</description></item><item><title>Flexbox cross browser</title><link>https://blog-570662.gitlab.io/flexbox-cross-browser/</link><pubDate>Fri, 16 Aug 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/flexbox-cross-browser/</guid><description>&lt;p&gt;Despite having been around for a while and having been through a couple of revisions, its support across browsers can vary greatly. From &amp;ldquo;Candidate Recommendation&amp;rdquo; on Chrome/Opera, &amp;ldquo;legacy flexbox&amp;rdquo; on Firefox and no support at all on IE9 and earlier.&lt;/p&gt;
&lt;p&gt;Making flexbox work consistently across browsers was a challenge for us on a recent project, but I have found a solution that seems to work quite well.&lt;/p&gt;
&lt;p&gt;Below is an SCSS @mixin that will attempt to handle compatibility between CR and legacy cross browsers flexbox.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@mixin flex($content: flex-start, $items: stretch, $direction: row, $wrap: wrap) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $packLegacy == flex-start {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: start;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $packLegacy == flex-end {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: end;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $alignLegacy ==flex-start {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: start;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $alignLegacy == flex-end {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: end;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $directionLegacy: normal;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $oritentLegacy == row {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: horizontal;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $oritentLegacy == column {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: vertical;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** SAFARI **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -webkit-box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** FIREFOX LEGACY **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -moz-box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-direction: $directionLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** LEGACY **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-direction: $directionLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** IE 10+ **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -ms-flexbox;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** CHROME **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -webkit-flex;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** NATIVE **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: flex;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;} //@mixin flex
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@mixin flexItem($width) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; min-height: 0;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Firefox however only half supports flexbox (all revisions) and to get around this I would recommend using &lt;a class="link" href="http://modernizr.com/" title="Modernizr"
 target="_blank" rel="noopener"
 &gt;Modernizr&lt;/a&gt; as this will add the class &amp;ldquo;no-flexbox&amp;rdquo; to the &lt;html&gt; tag. This provides us with a simple work around that allows non flexbox supporting browsers render correctly by using specifically crafted and targeted CSS for non-flexbox browsers&lt;/p&gt;
&lt;p&gt;I found that IE9 support could be implemented using the &lt;a class="link" href="http://flexiejs.com/" title="FlexieJS"
 target="_blank" rel="noopener"
 &gt;flexie&lt;/a&gt; javascript plugin. In IE8 M&lt;a class="link" href="http://modernizr.com/" title="Modernizr"
 target="_blank" rel="noopener"
 &gt;odernizr&lt;/a&gt; will add the class &amp;ldquo;no-flexboxlegacy&amp;rdquo; which can again allow you to create targeted CSS that wont affect your Flexbox layout.&lt;/p&gt;
&lt;p&gt;For a great overview of the &amp;ldquo;CR&amp;rdquo; of flexbox, CSS Tricks has an amazingly comprehensive coverage of the functionality here &lt;a class="link" href="http://css-tricks.com/snippets/css/a-guide-to-flexbox/" target="_blank" rel="noopener"
 &gt;http://css-tricks.com/snippets/css/a-guide-to-flexbox/&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Creating Custom Routes in Silverstripe 3.1</title><link>https://blog-570662.gitlab.io/creating-custom-routes-silverstripe/</link><pubDate>Wed, 31 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/creating-custom-routes-silverstripe/</guid><description>&lt;p&gt;We wanted to create a Route to our custom Products Controller in our products module for SilverStripe 3.1, such as: &amp;ldquo;&lt;a class="link" href="http://www.examplesite.com/products/" target="_blank" rel="noopener"
 &gt;http://www.examplesite.com/products/&lt;/a&gt;&lt;product-slug&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;However looking at the &lt;a class="link" href="http://doc.silverstripe.org/framework/en/3.1/topics/controller" title="Controller Documentation"
 target="_blank" rel="noopener"
 &gt;Controller Documentation&lt;/a&gt; it was not clear how to create a route without an Action being supplied. In our example above the action is not specified, as we just want to use &amp;lsquo;view&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Solution:&lt;/p&gt;
&lt;p&gt;Create a &lt;module-name&gt;/_config/routes.yml file containing the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Name: productsroutes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;After: &amp;#39;framework/routes#coreroutes&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Director:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rules:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;product&amp;#39;: &amp;#39;Product_Controller&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above will redirect any Url that starts with &amp;ldquo;/product&amp;rdquo; to our Product_Controller. Note that everything after the rule, so after &amp;ldquo;/product&amp;rdquo;, is used in the next bit for matching.&lt;/p&gt;
&lt;p&gt;Now we need to add &lt;code&gt;private static $url_handers&lt;/code&gt; to Product_Controller to match our path, so in this example we need to match &amp;ldquo;$Slug!&amp;rdquo; which will match &amp;ldquo;&lt;product-slug&gt;&amp;rdquo;. Note the ! means the slug is required. Of course we want to direct this to a specific action, in this case &amp;ldquo;view&amp;rdquo;, this gives us:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;private static $url_handlers = array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;$Slug!&amp;#39; =&amp;gt; &amp;#39;view&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now just add &amp;ldquo;view&amp;rdquo; to the $allow_actions and add the &amp;ldquo;view&amp;rdquo; function. This gives the final Product_Controller as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Product_Controller&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Page_Controller&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;url_handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;$Slug!&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;view&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowed_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;view&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SS_HTTPRequest&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Your&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;goes&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Handy note:&lt;/p&gt;
&lt;p&gt;You can put ?&lt;code&gt;debug_request=1 on the end of your URL to see how it determines which Controller to use.&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Disabling Cache in Silverstripe 3.1</title><link>https://blog-570662.gitlab.io/disabling-cache-silverstripe/</link><pubDate>Wed, 31 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/disabling-cache-silverstripe/</guid><description>&lt;p&gt;While working with Silverstripe we found ourselves having to run &amp;ldquo;?flush=1&amp;rdquo; a lot to clear the Cache. To switch it off, while you work, add the following to your mysite/_config.php:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SS_Cache::set_cache_lifetime(&amp;#39;default&amp;#39;, -1, 100);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Set up SilverStripe 3.1 using only Git (No Composer)</title><link>https://blog-570662.gitlab.io/set-up-silverstripe-3-1-using-only-git/</link><pubDate>Mon, 29 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/set-up-silverstripe-3-1-using-only-git/</guid><description>&lt;p&gt;We recently tried to use composer to set up SilverStripe 3.1, but ended up with a dependency nightmare. In order to work around this we decided to make use of Git submodules.&lt;/p&gt;
&lt;p&gt;First set up your Git repository and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next set up a site directory for the code inside your Git repository. Then navigate to &lt;a class="link" href="https://github.com/silverstripe/silverstripe-installer" target="_blank" rel="noopener"
 &gt;SilverStripe Installer&lt;/a&gt; in your browser and Download a copy. Extract files, and copy contents to site folder. Now we need to add the CMS and Framework. Navigate in a browser to the Git Hub repositories for &lt;a class="link" href="https://github.com/silverstripe/silverstripe-cms" target="_blank" rel="noopener"
 &gt;CMS&lt;/a&gt; and &lt;a class="link" href="https://github.com/silverstripe/silverstripe-framework" target="_blank" rel="noopener"
 &gt;Framework.&lt;/a&gt; Now copy the HTTPS clone URL for each project and run the following, to add these as Git sub modules.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add https://github.com/silverstripe/silverstripe-framework.git site/framework
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add https://github.com/silverstripe/silverstripe-cms.git &amp;lt;path-to-site&amp;gt;site/cms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now delete mysite/_config.php and load the site. Follow the normal install instructions displayed and you will have a running version of &lt;a class="link" href="http://www.silverstripe.org/" title="SilverStripe"
 target="_blank" rel="noopener"
 &gt;SilverStripe 3.1&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Enabling MYSQL_CLIENT_INTERACTIVE with Doctrine 2 on Rackspace Cloud Database</title><link>https://blog-570662.gitlab.io/mysql-client-interactive-with-doctrine-on-rackspace/</link><pubDate>Fri, 26 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/mysql-client-interactive-with-doctrine-on-rackspace/</guid><description>&lt;p&gt;We recently ran into problem using Doctrine 2 connecting to a Rackspace Cloud Database using the MySqli Driver.&lt;/p&gt;
&lt;p&gt;Problem:&lt;/p&gt;
&lt;p&gt;We have a long running PHP script that can sometimes run for hours at a time whilst processing information. This script requires a connection to a database, but has long periods of inactivity where there is no actual interaction with MySQL. By default MySQL uses the &amp;ldquo;wait_timeout&amp;rdquo; setting which states, how long an inactive connection can exist before it is killed. This is normally fine with web pages requests, as it is usually a short lived request. Unfortunately you do not have the ability to alter this setting when using Rackspaces Cloud Database.&lt;/p&gt;
&lt;p&gt;Solution:&lt;/p&gt;
&lt;p&gt;When using the MySQLi extension you can create a connection in &amp;ldquo;interactive mode&amp;rdquo; by passing the &amp;ldquo;MYSQLI_CLIENT_INTERACTIVE&amp;rdquo; flag, which will then use the &amp;ldquo;interactive_timeout&amp;rdquo; setting. On Rackspace this is set to 8 hours!&lt;/p&gt;
&lt;p&gt;Annoyingly Doctrine does not allow you to pass any flags to the MySQLi Connection. So we overrode Doctrine\DBAL\Driver\Connection with our own &lt;a class="link" href="https://github.com/zucchi/ZucchiDoctrine/blob/master/src/ZucchiDoctrine/Driver/Mysqli/MysqliConnection.php" title="ZucchiDoctrine/Driver/Mysqli/MysqliConnection.php"
 target="_blank" rel="noopener"
 &gt;Driver&lt;/a&gt; which then allows us to pass a &amp;ldquo;flags&amp;rdquo; parameter through.&lt;/p&gt;
&lt;p&gt;Feel free to look at some of the other helpful features in we have added to Doctrine 2 here: &lt;a class="link" href="https://github.com/zucchi/ZucchiDoctrine" title="ZucchiDoctrine"
 target="_blank" rel="noopener"
 &gt;ZucchiDoctrine&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Installing PECL extensions for Zend Server 6</title><link>https://blog-570662.gitlab.io/installing-pecl-extensions-zend-server-6/</link><pubDate>Mon, 13 May 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/installing-pecl-extensions-zend-server-6/</guid><description>&lt;p&gt;Recently we have revisited using Zend Server for some of our projects and decided to give the new version 6 a chance to prove itself.&lt;/p&gt;
&lt;p&gt;Overall its a big improvement over version 5. There are still some things that are extremely annoying but we have decided that we can overlook them.&lt;/p&gt;
&lt;p&gt;However there is one thing that we couldn&amp;rsquo;t do without. By default you will find that a number of PECL extensions will not install out of the box (at least this is what we experience using the Debian based install).&lt;/p&gt;
&lt;p&gt;To fix this you will need to make sure you install the additional packages in ubuntu&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;php-5.4-source-zend-server&lt;/strong&gt; or &lt;strong&gt;php-5.3-source-zend-server&lt;/strong&gt; depending on the php version you are using&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;autoconf&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;build-essential&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once this is done you should now be able to install extensions from PECL without too much hassle.&lt;/p&gt;</description></item><item><title>Better Output for MySQL Select Command Using \G</title><link>https://blog-570662.gitlab.io/better-output-mysql-command-line/</link><pubDate>Wed, 24 Apr 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/better-output-mysql-command-line/</guid><description>&lt;p&gt;If you ever find yourself using MySQL via command line and end up with something like this:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/04/mysql-command-line.jpg" target="_blank" rel="noopener"
 &gt;&lt;img alt="mysql-command-line" class="gallery-image" data-flex-basis="667px" data-flex-grow="278" height="319" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line.jpg" srcset="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line_hu_a6381c4c6e0774e6.jpg 800w, https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line.jpg 887w" width="887"&gt;&lt;/a&gt; And thought there must be another way, well here it is: Use &lt;strong&gt;\G&lt;/strong&gt; instead of &lt;strong&gt;;&lt;/strong&gt; at the end of your select command.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;select * from CHARACTER_SETS\G
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Below is an image of the output from this select:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/04/mysql-nice-output.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="mysql-nice-output" class="gallery-image" data-flex-basis="489px" data-flex-grow="204" height="250" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-nice-output.png" width="510"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy Querying!&lt;/p&gt;</description></item><item><title>Introducing ZuQ - A Simple ZeroMQ Queuing Daemon</title><link>https://blog-570662.gitlab.io/introducing-zuq/</link><pubDate>Tue, 19 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/introducing-zuq/</guid><description>&lt;p&gt;We recently had the need to create a queuing system to replace an implementation of RabbitMQ that was being used on a previous project. The reasoning behind this is that the requirements of the project required a very custom implementation of a queuing system that would drastically alter in architecture as the project grew and RabbitMQ just wasn&amp;rsquo;t going to fit the bill. However to start with we required something super simple and efficient that could be expanded and developed as required. After a little investigation and a lot of recommendation from others we decided to use ZeroMQ as our transport layer for that very reason, as we could build something which could span across multiple servers and was fast.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/clientQueue.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="This is a simple Queue and Client Diagram" class="gallery-image" data-flex-basis="1082px" data-flex-grow="450" height="165" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/introducing-zuq/clientQueue.png" width="744"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The diagram above helps describe our basic queuing system. We have a queue daemon that is continuously listening for connections and two clients, one that populates the queue and the other that retrieves from the queue.&lt;/p&gt;
&lt;h2 id="the-clients"&gt;The Clients
&lt;/h2&gt;&lt;p&gt;Each client is written in PHP and uses a 0mq socket to communicate with a service, in this case our queue service. We used a SOCKET_REQ type of socket in order to have a request/response communication with our queue service.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;function client_socket(\ZMQContext $context)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKET_REQ used to create a client that sends requests to and receive from a service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client = new \ZMQSocket($context,\ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;connect(&amp;#34;tcp://localhost:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKOPT_LINGER = 0 Configure socket to not wait at close time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;setSockOpt(\ZMQ::SOCKOPT_LINGER, 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $client;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;public function injectIntoQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $msg = &amp;#34;This is a message&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $retries_left = 3;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  while ($retries_left) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    // We send a request, then we wait to get a reply
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $expect_reply = true;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        while ($expect_reply) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        // Poll socket for a reply, with timeout
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll = new \ZMQPoll();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll-&amp;gt;add($client, \ZMQ::POLL_IN);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $events = $poll-&amp;gt;poll($read, $write, 2500);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // If we got a reply, process it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            if ($events &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // We got a reply from the server, must match sequence
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $reply = $client-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                if (intval($reply) == $msg) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $retries_left = 0;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                    $expect_reply = false;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } elseif (--$retries_left == 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            break;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;               // Old socket will be confused; close it and open a new one
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                // Send request again, on new socket
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;You can see from the code above, have a 3 strike rule. The reasoning behind this is that if the client fails to connect to the queue service more than 3 times, we can stop trying to inject into the queue and move on to the next item. As we ultimately intend to adapt the &lt;a class="link" href="http://zguide.zeromq.org/page:all#Client-side-Reliability-Lazy-Pirate-Pattern" title="lazy pirate pattern"
 target="_blank" rel="noopener"
 &gt;lazy pirate pattern&lt;/a&gt; we have made it so that if the socket times out, we can then create a new socket and retry. Without this, as the architecture becomes more complicated we may then end up in a situation where we might have errors, thus the recommend solution is to create a new socket. Once the client has sent its message to the queue, we poll for a response (i.e. which is the message we sent returned back). Once we have a response that is valid, meaning that the queue has been populated, we can stop polling until the next message.&lt;/p&gt;
&lt;p&gt;The Frontend Client&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protected function getFromQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker = new \ZMQSocket($context, \ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Set random identity to make tracing easier
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;connect(&amp;#34;tcp://localhost:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Tell queue we&amp;#39;re ready for work
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;send(&amp;#34;ready&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $reply = $worker-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $reply;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;Our Frontend Client is much simpler as it is part of a process that is being continually updated, therefore it doesn&amp;rsquo;t need the same connection retries are the Backend Client. We simply send &amp;ldquo;ready&amp;rdquo; to the queue system and if the queue is populated it will return us the first item.&lt;/p&gt;
&lt;h2 id="the-queuing-service"&gt;The Queuing Service
&lt;/h2&gt;&lt;p&gt;This is a continuously running executable created using c++.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::context_t context(1);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t frontend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t backend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backend.bind(&amp;#34;tcp://*:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;frontend.bind(&amp;#34;tcp://*:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;We create two sockets of type ZMQ_ROUTER which is an advanced pattern used for extending request/reply sockets. This means when we improve our queuing system we will be able to route packets to specific recipients using an address in the message.&lt;/p&gt;
&lt;p&gt;After creating our sockets, we initialise them&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Initialize poll set
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::pollitem_t items [] = {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { frontend, 0, ZMQ_POLLIN, 0 },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { backend, 0, ZMQ_POLLIN, 0 }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//poll the sockets - this seems to poll both sockets at the same time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::poll (items, 2, -1);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h2 id="backend-handler"&gt;Backend Handler
&lt;/h2&gt;&lt;p&gt;If we get a message from the backend, we check the contents to see if it contains purge at which point we empty the queue, otherwise we push the msg contents onto the queue. Finally we send the message back to the backend to show that the message has been received.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//receive msg from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [1].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string client_addr = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //allow the backend to purge the queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;purge&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; while (!queue.empty()) queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.push(msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send response back to the backend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, client_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (backend, msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="frontend-handler"&gt;Frontend Handler
&lt;/h2&gt;&lt;p&gt;If we get the &amp;ldquo;ready&amp;rdquo; message from the frontend client, we pop a message off the queue and return it to the frontend client. If the queue is empty we send an &amp;ldquo;empty&amp;rdquo; message back instead.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Handle activity on frontend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [0].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from worker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string worker_addr = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (frontend); string queueMsg;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;ready&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(queue.size() &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = queue.front();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = &amp;#34;empty&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send reply to worker with contents of queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, worker_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (frontend, queueMsg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;That is our complete queuing system using ZeroMQ with PHP and C++.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Using the above has allowed us to create a very simple in memory queue daemon that we can use to quickly pass data from one system to a another. On the whole it works well and we are looking to expand on it in the near future to increase both its functionality and scalability.&lt;/p&gt;
&lt;p&gt;You can find the queueing daemon (christened as &amp;ldquo;ZuQ&amp;rdquo;) on github @ &lt;a class="link" href="https://github.com/zucchi/ZuQ" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/ZuQ&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Glorious Gluster - How to setup GlusterFS on Rackspace Cloud and Ubuntu 12.10</title><link>https://blog-570662.gitlab.io/gluster-licious/</link><pubDate>Fri, 15 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/gluster-licious/</guid><description>&lt;p&gt;A few of our projects recently called for a distributed file-system that provided high availability and redundancy. After a tip off from a fellow techie and a quick browse around the net it appeared that a solution called GlusterFS appeared to tick all the boxes for what we were wanting.&lt;/p&gt;
&lt;p&gt;However setting it up turned out not to be as trivial as I had originally anticipated. I&amp;rsquo;m going to try and put down the process we have evolved for setting it up on Ubuntu in the cloud&lt;/p&gt;
&lt;p&gt;A couple of things to clear up first.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We are using Rackspace for our cloud but beyond the setup of the servers it should still be relevant&lt;/li&gt;
&lt;li&gt;There are a number of ways to interact with Rackspaces set up but for this we are going to use the cloud control panel&lt;/li&gt;
&lt;li&gt;We use Ubuntu as our preferred server which means that our config tends to be all over the place compared to other guides&lt;/li&gt;
&lt;li&gt;You will need to set up a minimum of 2 servers and a separate block storage device for each.&lt;/li&gt;
&lt;li&gt;We have set up and broken a few different variations of gluster setup so far and make no guarantees that the setup in this blog is infallable but its the best wehave so far.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="setting-up-the-hardware"&gt;Setting up the hardware
&lt;/h2&gt;&lt;p&gt;First things first. We are going to need to set up are some servers.&lt;/p&gt;
&lt;p&gt;Feel free to create any size server you want. Just make sure to select Ubuntu 12.10 (or whatever version you may have that is newer).&lt;/p&gt;
&lt;p&gt;You will also need to define a new network to work with. We use this to isolate the traffic between the nodes of our new gluster.&lt;/p&gt;
&lt;p&gt;You can create a new network when creating the first of your servers. On the creation page under the networks heading you can find a &amp;ldquo;Create Network&amp;rdquo; button.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/create-network.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="create-network" class="gallery-image" data-flex-basis="403px" data-flex-grow="168" height="316" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/create-network.png" width="531"&gt;&lt;/a&gt;Hopefully this should be quite self explanatory. Now when you create subsequent servers you will then have the option to attach your new network (&amp;ldquo;GlusterNet&amp;rdquo; in my example).&lt;/p&gt;
&lt;p&gt;Once the two starting nodes have been created then you need to add some additional block storage to store your data on. Make sure that you create blocks that have sufficient capacity for your needs. Something else to consider is using High Performance SSD storage. Its a little on the pricy side but well worth the expense if you are trying to eak out every ounce of performance from the implementation.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/block-storage.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="block-storage" class="gallery-image" data-flex-basis="238px" data-flex-grow="99" height="456" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/block-storage.png" width="453"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will then need to attach one to each of your servers.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/attach-storage.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="attach-storage" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="426" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/attach-storage.png" width="568"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once attached you will be able to see the details of the block mount point from the block storage details page.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/storage-details.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="storage-details" class="gallery-image" data-flex-basis="366px" data-flex-grow="152" height="297" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/storage-details.png" width="453"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Make a note of the mount point (in this case &amp;ldquo;/dev/xvdb&amp;rdquo;) as we will need that in a minute.&lt;/p&gt;
&lt;h2 id="prepare-the-server"&gt;Prepare the Server
&lt;/h2&gt;&lt;p&gt;Now that we have a the hardware ready we can shell into a server to set it up.&lt;/p&gt;
&lt;p&gt;First you need to shell into your server and update its OS as the images provided by most cloud supplier tends not to have the latest patches and updates. In our case it&amp;rsquo;s as simple as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that&amp;rsquo;s done we then need to prepare the Block Storage device ( henceforth refered to as a &amp;ldquo;brick&amp;rdquo;)&lt;/p&gt;
&lt;p&gt;if you run fdisk -l  you should see that an entry that looks something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb: 107.4 GB, 107374182400 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;255 heads, 63 sectors/track, 13054 cylinders, total 209715200 sectors
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Units = sectors of 1 * 512 = 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk identifier: 0x00000000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb doesn&amp;#39;t contain a valid partition table
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This indicates that our brick needs a partition table and formatting. We can achieve this be doing the following&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Building a new DOS disklabel with disk identifier 0xe7da4288.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Changes will remain in memory only, until you decide to write them.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;After that, of course, the previous content won&amp;#39;t be recoverable.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command (m for help): n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition type:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; p primary (0 primary, 0 extended, 4 free)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; e extended
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Select (default p): p
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition number (1-4, default 1): 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;First sector (2048-209715199, default 2048): 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using default value 2048
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Last sector, +sectors or +size{K,M,G} (2048-209715199, default 209715199): 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using default value 209715199
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command (m for help): w
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The partition table has been altered!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Calling ioctl() to re-read partition table.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Syncing disks.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve highlighted the prompts and my responses. All we are doing here is creating a default partition table that has a single partition which uses up the whole disk.&lt;/p&gt;
&lt;p&gt;now running fdisk -l  should give us something that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb: 107.4 GB, 107374182400 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;43 heads, 44 sectors/track, 110843 cylinders, total 209715200 sectors
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Units = sectors of 1 * 512 = 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk identifier: 0xe7da4288
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Device Boot Start End Blocks Id System
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/xvdb1 2048 209715199 104856576 83 Linu
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can now see we have a valid device of  /dev/xvdb1 that we can mount_._ However we need to create a valid filesystem on the new brick before we can mount it. I have been doing this with Ext4 rather than XFS (which is the recommened filesystem from gluster), this is mainly down to the fact that when i tried using XFS I kept getting some issues with performance and access. I&amp;rsquo;m sure that with further investigation I could resolve this but as of yet haven&amp;rsquo;t had chance to. So far though I have had zero issues using Ext4. To create the filesystem we run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkfs.ext4 -j /dev/xvdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, create a folder to mount to, easily done by executing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, the simplest way to mount the device is via your /etc/fstab by adding the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/xvdb1 /glusterfs/brick ext4 defaults 1 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and running mount -a  as root (this will also mean that it mounts on boot for you automatically as well.)&lt;/p&gt;
&lt;p&gt;Next we need to install the latest gluster version. At the time of writing this was v3.3.1. You can find a version to suit your OS at &lt;a class="link" href="http://www.gluster.org/download" target="_blank" rel="noopener"
 &gt;http://www.gluster.org/download&lt;/a&gt;. If you are using Ubuntu you can do the following&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install software-properties-common
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install glusterfs-server glusterfs-client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By this point you will now have a single working server to continue on your going to need to set up your second server ready to create your new volume.&lt;/p&gt;
&lt;p&gt;Once you have your second (or third, fourth, etc) setup its a good idea to add a reference to each one of them to your /etc/hosts  file. This is not really necessary and you can just use the IP addresses of each server but it saves you having to remember each IP and makes it easier to identify.&lt;/p&gt;
&lt;p&gt;Remember that we are going to be working with the new network interface you created earlier (i.e &amp;ldquo;GlusterNet&amp;rdquo;). to get the IP of your GlusterNet interface a quick ifconfig will show you an interface with an IP that matched the CIDR from earlier. In my case I now have 2 IPs of 192.168.3.1 &amp;amp; 192.168.3.2.&lt;/p&gt;
&lt;p&gt;So now I add the following lines to my /etc/hosts  file&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.3.1 gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.3.2 gluster2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="creating-our-volume"&gt;Creating our volume
&lt;/h2&gt;&lt;p&gt;Now that the servers are prepared we can now play with the the tool gluster.This tool is a life saver in getting everything configured quickly and you can easily get a list of what its capable of by running gluster help. Now Im not going to take you through every command and option and would recomend reading the gluster manual to learn more.&lt;/p&gt;
&lt;p&gt;What this tool actually does is help generate and manipulate all the required config that is then stored at /var/lib/glusterd/.&lt;/p&gt;
&lt;p&gt;Firstly we need to tell gluster is that we have a pool of servers that will communicate with each other. Gluster refers to these as peers. To do this you need to run gluster peer probe gluster2 on each server for each server that will be used, replacing &amp;ldquo;gluster2&amp;rdquo; with the name names you defined in your /etc/hosts  file. This will then create the appropriate files at /var/lib/glusterd/peers/&lt;/p&gt;
&lt;p&gt;Now that all our peers have been defined we can get to actually creating the new distributed volume. This however requires a little consideration as there are some decisions you need to make.&lt;/p&gt;
&lt;p&gt;If we take a look at the help for creating a new volume we can see that we need to decide on what options to use&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume create &amp;lt;NEW-VOLNAME&amp;gt; [stripe &amp;lt;COUNT&amp;gt;] [replica &amp;lt;COUNT&amp;gt;] [transport &amp;lt;tcp|rdma|tcp,rdma&amp;gt;] &amp;lt;NEW-BRICK&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;&lt;NEW-VOLNAME&gt;  - what are we going to name our volume&lt;/li&gt;
&lt;li&gt;[stripe &lt;COUNT&gt;] [replica &lt;COUNT&gt;] - are we going to crate a striped or replicated volume and how many &amp;ldquo;bricks&amp;rdquo; are we going to create this volume with&lt;/li&gt;
&lt;li&gt;[transport {tcp|rdma|tcp,rdma&amp;gt;] - What transport protocol do you want the peers to communicate with&lt;/li&gt;
&lt;li&gt;&lt;NEW-BRICK&gt; - which servers/bricks do you want to use.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;for more information on how to create you volume and what all the options mean have a look at these links&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Replicated_Volumes" target="_blank" rel="noopener"
 &gt;http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Replicated_Volumes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Striped_Volumes" target="_blank" rel="noopener"
 &gt;http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Striped_Volumes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;for our purposes we are going to run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gluster volume create myvolume replica 2 transport tcp gluster1:/glusterfs/brick gluster2:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This now creates a new volume that spans both of our servers. you can confirm that this is the case by running gluster volume info  and you should get something that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Volume Name: myvolume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Type: Replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Volume ID: d3dd24fd-9482-44c3-9503-24291fad8193
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Status: Created
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Number of Bricks: 1 x 2 = 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Transport-type: tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Bricks:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Brick1: gluster1:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Brick2: gluster2:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;running this on both servers should give you the same results.&lt;/p&gt;
&lt;p&gt;What you will now find is that the gluster  command has created a plethora of files at /var/lib/glusterd/vols/myvolume/. As you work with gluster more and more you will find yourself drawn to these files as they control all the different aspects of how the volume works and performs. Most importantly we will need some information from these files when we come to configure a client to mount the volume.&lt;/p&gt;
&lt;p&gt;All that is left to do now is start the volume which can be easily done with a quick gluster volume start myvolume&lt;/p&gt;
&lt;p&gt;At this point we have now completed setting up our volume but we need to add some security. I would strongly recommend setting up a firewall using ufw to control access to the server. The easiest way to do this is to allow all traffic on your &amp;ldquo;GlusterNet&amp;rdquo; network interface as only the servers you attach to that network will have access. you can find a guide to using ufw at &lt;a class="link" href="https://help.ubuntu.com/12.10/serverguide/firewall.html" target="_blank" rel="noopener"
 &gt;https://help.ubuntu.com/12.10/serverguide/firewall.html&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="mounting-a-client"&gt;Mounting a Client
&lt;/h2&gt;&lt;p&gt;Now that we have a working volume we need to add some clients. To do this you will need to create a new server as above that is attached to the &amp;ldquo;GlusterNet&amp;rdquo; network but without the block storage (unless you really want it that is).&lt;/p&gt;
&lt;p&gt;Make sure to add your gluster dfinitions to your /etc/hosts file&lt;/p&gt;
&lt;p&gt;Once you have your new client server ready we can install the gluster client&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install software-properties-common
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install glusterfs-client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve seen a number of different guides that tell you to install glusterfs-server as well but I have as yet had no need to as it all works without it.&lt;/p&gt;
&lt;p&gt;Now there are a lot of way that you can mount your new Gluster volume. I have tried a few and have had varying results. What I have found is that the best way is to create a volume file. To do this we create a new file at /etc/glusterfs.vol.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type protocol/client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option transport-type tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-host gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-subvolume /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option username &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option password &amp;lt;password&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type protocol/client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option transport-type tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-host gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-subvolume /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option username &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option password &amp;lt;password&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type cluster/replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes gluster1 gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume writebehind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type performance/write-behind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option cache-size 1MB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type performance/io-cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option cache-size 400MB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes writebehind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What you will notice is that there is a &lt;username&gt;  and &lt;password&gt;  required for this to work. You can find these details on one of your peer servers in the file /var/lib/glusterd/vols/myvolume/trusted-myvolume-fuse.vol.&lt;/p&gt;
&lt;p&gt;This /etc/gluster.vol file is basically going to inform the gluster-client software about how to connect to the gluster volume and all the available nodes to connect to. This provides us with some level of fail-over so should one node become unavailable the gluster client will seamlessly switch to a different one. It also allows us to define additional &amp;ldquo;&lt;a class="link" href="http://www.gluster.org/community/documentation/index.php/Translators" title="translators"
 target="_blank" rel="noopener"
 &gt;translators&lt;/a&gt;&amp;rdquo; such as the &lt;a class="link" href="http://www.gluster.org/community/documentation/index.php/Translators/performance/io-cache" title="performance-io"
 target="_blank" rel="noopener"
 &gt;performance-io&lt;/a&gt; one that you can see here. I would strongly recommend reading through the available translators to see which may be useful to you.&lt;/p&gt;
&lt;p&gt;Now one of the main issues you will find with Ubuntu is that it will fail on boot if you try to add this mount to your fstab. To get around this you can use Upstart. if you create the following file at /etc/init/glusterfs-mount.conf  making sure to change &lt;interface&gt;  to the interface for your GlusterNet network (i.e. eth0 or eth1 or eth2, you get the idea)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;author &amp;#34;Matt Cockayne&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;description &amp;#34;Mount GlusterFS after networking available&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start on net-device-up IFACE=&amp;lt;interface&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop on stopping network
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop on starting shutdown
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;script
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    mount -t glusterfs /etc/glusterfs.vol /glusterfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end script
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see we are using a straight mount command. The magic is that this will not be executed until the start clause validates which in this case is not until the network interface for &amp;ldquo;GlusterNet&amp;rdquo; is up and running properly. You will also see that we are mounting the /etc/gluster.vol  file to /gluster  (remember to create this folder to mount to) rather than mounting a network path as you might when mounting an NFS share.&lt;/p&gt;
&lt;p&gt;If you wanted you could also add more to your upstart script to handle clean un-mounting of gluster thus allowing you to then use the service gluster-mount (start|stop|restart)  commands&lt;/p&gt;
&lt;p&gt;A quick reboot of the client server should confirm that it boots successfully and you will now end up with your volume mounted at /gluster. You can now test this by creating a new file. I tend to create an empty file at /gluster/mounted  just so I have a quick reference that the folder is mounted. Once that&amp;rsquo;s created if you now go and take a look at the /gluster/brick  on your &amp;ldquo;peers&amp;rdquo; you should see that there is now a file called &amp;ldquo;mounted&amp;rdquo; sat there looking all smug that it worked.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats
&lt;/h2&gt;&lt;p&gt;Some important things for you to be made aware of&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Never write directly to a brick. Make sure to write to the volume only through a configured client&lt;/li&gt;
&lt;li&gt;Beware of split-brain. &lt;a class="link" href="http://community.gluster.org/q/what-is-split-brain-in-glusterfs-and-how-can-i-cause-it/" target="_blank" rel="noopener"
 &gt;http://community.gluster.org/q/what-is-split-brain-in-glusterfs-and-how-can-i-cause-it/&lt;/a&gt; &lt;a class="link" href="http://www.gluster.org/2012/06/healing-split-brain/" target="_blank" rel="noopener"
 &gt;http://www.gluster.org/2012/06/healing-split-brain/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RTFM - Read The F***ing Manual. Gluster is big and complex and there is a lot for you to understand. You can download a copy of the manual from &lt;a class="link" href="http://www.gluster.org/wp-content/uploads/2012/05/Gluster_File_System-3.3.0-Administration_Guide-en-US.pdf" title="PDF"
 target="_blank" rel="noopener"
 &gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Docblock, Oh Docblock, wherefore art thou Docblock (hint: Zend Optimizer Plus lost them)</title><link>https://blog-570662.gitlab.io/docblock-docblock-wherefore-art/</link><pubDate>Fri, 01 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/docblock-docblock-wherefore-art/</guid><description>&lt;p&gt;tl;dr&amp;gt; I make a terrible assumption about Zend Optimizer+ and am corrected by Dominic in the comments;&lt;/p&gt;
&lt;p&gt;Terrible post title I know but its the best I could come up with.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve just come up for air after spending the majority of the day debugging some issues on our current development sandbox.&lt;/p&gt;
&lt;p&gt;Now our sandbox tends to be quite bleeding edge in some circumstances and as such we run a fair few bits of unstable code. On the sandbox in question we have been running PHP 5.4.11 and unfortunately we have struggled to get APC working with it just the way we need it to. The lack of APC tends to make this sandbox quite slow.&lt;/p&gt;
&lt;p&gt;We recently saw that Zend have open-sourced their OptimizerPlus extension (&lt;a class="link" href="https://github.com/zend-dev/ZendOptimizerPlus" title="https://github.com/zend-dev/ZendOptimizerPlus"
 target="_blank" rel="noopener"
 &gt;https://github.com/zend-dev/ZendOptimizerPlus&lt;/a&gt;) and that it was compatible with 5.4&amp;hellip;. Fantastic, or so we thought.&lt;/p&gt;
&lt;p&gt;So I added the new OptimiserPlus to the sandbox and everything was going swimmingly. That was until we had to run one of the utility scripts that we use to rebuild some of our data structures. These scripts make use of different parts of both Zend Framework and Doctrine which tend to rely on some heavy DocBlock annotations.&lt;/p&gt;
&lt;p&gt;Now having used both APC and Zend Server knowing that they done affect this kind of functionality I had expected that OptimizerPlus would be fine&amp;hellip;. Wrongo. It took me a good few hours of head scratching trying to figure out what had happened.&lt;/p&gt;
&lt;p&gt;It turns out that OptimizerPlus suffers from the same flaws that eAccellerator does and strips Docblocks when caching the bytecode. This results in Reflection returning false when you call methods such as `getDocComment()`.&lt;/p&gt;
&lt;p&gt;All in all its not the end of the world I just disable OptimizerPlus and have to wait till I can get APC working. Not my ideal scenario but I can live with it.&lt;/p&gt;
&lt;p&gt;Something that does concern me is that there is currently an RFC that has gone to vote (&lt;a class="link" href="https://wiki.php.net/rfc/optimizerplus" title="https://wiki.php.net/rfc/optimizerplus"
 target="_blank" rel="noopener"
 &gt;https://wiki.php.net/rfc/optimizerplus&lt;/a&gt;) about integrating OptimizerPlus into the PHP 5.5 distribution. While this is great I do worry how many other things may break and will they be picked up and fixed for the 5.5 release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; Since writing this post the RFC has finished being voted upon and has been approved. You can expect to see Optimizer Plus appearing bundled with PHP soon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (15th Mar 13):&lt;/strong&gt; Thanks to Dominics&amp;rsquo; comment I now know that you can tell Optimizer+ to retain your Docblocks by setting your config using&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zend_optimizerplus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save_comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;PHPDoc&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;optimized&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Disabling&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Doc Comments&amp;#34;&lt;/span&gt; &lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;frameworks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Doctrine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ZF2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PHPUnit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zend_optimizerplus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PHPDoc&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;SHM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Doc Comments&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;save_comments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;don&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;t need them anyway.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;ll teach me to write a blog post without investigating more first.&lt;/p&gt;</description></item><item><title>Our Redmine install died, We all cried!</title><link>https://blog-570662.gitlab.io/redmine-install-died-we-cried/</link><pubDate>Sat, 23 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/redmine-install-died-we-cried/</guid><description>&lt;p&gt;We have been using redmine for quite a long time and a few months ago attempted to upgrade from 1.3 to 2.something. Unfortunately I (quite typically) borked the installation and since then its been hobbling along after my attempts to fix it left it crippled.&lt;/p&gt;
&lt;p&gt;Yesterday it finally gave up the fight and my attempts to resurrect the installation were futile. After a quick funeral (the eulogy was very touching), and wake in a nearby emporium of alcoholic beverages to commiserate our loss, I set about trying to figure out what to do next.&lt;/p&gt;
&lt;h2 id="alternatives"&gt;Alternatives
&lt;/h2&gt;&lt;p&gt;Now while Redmine is a worthy tool and has always managed to do what I needed in the past, recently its just not cut the mustard. I&amp;rsquo;ve kept toying with the idea of creating our own project management system but as with all in-house projects that we dream up its just never going to happen.&lt;/p&gt;
&lt;p&gt;A quick google around our options are to either go for a hosted solution (not possible as we have some very specific requirements regarding our SCM that mean we have to host our own repos for client work) or Redmine (or chilli project).&lt;/p&gt;
&lt;p&gt;Yes we looked at a number of other management tools and of them all Redmine is still the closes to what we needed.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation
&lt;/h2&gt;&lt;p&gt;So I spin up a new server instance of ubuntu 12.10 on the cloud and get to work installing the latest version.&lt;/p&gt;
&lt;p&gt;As root I then run through these steps (you should assume that ALL of these steps require you to be root and files should be owned by root)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# update/upgrade base installation of ubuntu packages&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;upgrade&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install the requisite scm tools that we use&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="n"&gt;subversion&lt;/span&gt; &lt;span class="n"&gt;mercurial&lt;/span&gt; &lt;span class="n"&gt;cvs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up ruby&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;ruby&lt;/span&gt; &lt;span class="n"&gt;rubygems&lt;/span&gt; &lt;span class="n"&gt;libruby&lt;/span&gt; &lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up apache &amp;amp; mysql&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="n"&gt;libapache2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;passenger&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="n"&gt;libmysqlclient&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install imagemagick and the magick wand&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;imagemagick&lt;/span&gt; &lt;span class="n"&gt;libmagickcore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;libmagickwand5&lt;/span&gt; &lt;span class="n"&gt;libmagickwand&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# create our user and database in mysql &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# replace uniquePassword with your own password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;create user &amp;#39;redmine&amp;#39;@&amp;#39;localhost&amp;#39; identified by &amp;#39;uniquePassword&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;create database redmine&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grant all on redmine.* to &amp;#39;redmine&amp;#39;@&amp;#39;localhost&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;flush privileges&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# clone redmine code to target location&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set apache as the owner of redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;chown&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# move into our new redmine folder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up your database configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;production:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; adapter: mysql2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; database: redmine
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; host: localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; username: redmine
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; password: uniquePassword
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install bundler gem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;bundler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# use bundler to set up redmine installation and without specified dependencies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;without&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="n"&gt;sqlite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up our secret token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;generate_secret_token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up our database and load default configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;migrate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;load_default_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# edit /etc/apache2/sites-available/default
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost *:80&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin webmaster@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; DocumentRoot /usr/local/share/redmine/public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /usr/local/share/redmine/public&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options Indexes FollowSymLinks MultiViews
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride All
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory &amp;#34;/usr/lib/cgi-bin&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # Possible values include: debug, info, notice, warn, error, crit,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # alert, emerg.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# restart apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That should be enough for you to have a working installation of redmine ready for you to use/customise&lt;/p&gt;
&lt;h2 id="additional-config"&gt;Additional Config
&lt;/h2&gt;&lt;p&gt;We typically have additional steps that we would configure for our own installation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# add plugin assets folder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir /usr/local/share/redmine/public/plugin_assets
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown www-data:www-data /usr/local/share/redmine/public/plugin_assets
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# enable some additional apache modules
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2enmod rewrite
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# disable mod ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2dismod ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# install gnutls 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install libapache2-mod-gnutls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# install ssl certificate bundle and key (this assumes that you have already copied the key and bundle to ~/)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv ~/my_certificate.bnd /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod 0644 /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv ~/my_certificate.crt /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod 0600 /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# now configure your /etc/apache2/sites-available/default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;IfModule mod_gnutls.c&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost _default_:443&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin webmaster@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; DocumentRoot /usr/local/share/redmine/public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /usr/local/share/redmine/public&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options Indexes FollowSymLinks MultiViews
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride All
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # Possible values include: debug, info, notice, warn, error, crit, alert, emerg.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/ssl_access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSEnable On
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSCertificateFile /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSKeyFile /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSPriorities NORMAL:!DHE-RSA:!DHE-DSS:!AES-256-CBC:%COMPAT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/IfModule&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Add some Rails / Passenger specific config to /etc/apache2/sites-available/default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RailsEnv production
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerDefaultUser www-data
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerSpawnMethod smart
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerPoolIdleTime 300
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerMaxRequests 5000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerStatThrottleRate 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerHighPerformance On
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# change your /etc/apache2/sites-available/default to redirect to ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost *:80&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin sysadmin@zucchi.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteEngine On
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteCond %{HTTPS} off
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# enable your new default-tls vhost and restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2ensite default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# setup &amp;amp;amp; configure email
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# when prompted select &amp;#34;internet site&amp;#34; and enter the domain you are hosting redmine from i.e. mysite.co.uk)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install postfix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# create config file and uncomment the production settings for sendmail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /usr/local/share/redmine/config/configuration.yml.example /usr/local/share/redmine/config/configuration.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vim /usr/local/share/redmine/config/configuration.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;production:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; email_delivery:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; delivery_method: :sendmail
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;#install pixel cookers theme cos we like it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git://github.com/pixel-cookers/RedmineThemePixelCookers.git /usr/local/share/redmine/public/themes/pixel-cookers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Debug PHP CLI on Remote Server with Xdebug and PHPStorm</title><link>https://blog-570662.gitlab.io/debug-cli-remote-server/</link><pubDate>Wed, 06 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/debug-cli-remote-server/</guid><description>&lt;p&gt;This was a head scratcher when I ran into this yesterday and I thought I would share my solution to the following scenario:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;I need to debug PHP Command Line script, located on Remote LAMP Virtual WebServer running in Virtual Box with a Shared Folder, using local PHPStorm 5.0.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The solution:&lt;/p&gt;
&lt;p&gt;You first must set PHPStorm to use remote file paths. To set these go to the following:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHPStorm -&amp;gt; Peferences -&amp;gt; PHP -&amp;gt; Servers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This gives the following display:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/02/PHPStorm-Peferences.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="PHPStorm Peferences" class="gallery-image" data-flex-basis="656px" data-flex-grow="273" height="312" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences.png" srcset="https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences_hu_a6aad5ce5c233f20.png 800w, https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences.png 854w" width="854"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Replace the Name, Host and Absolute path on the server, to match your own settings. Note keep the Name and Host the same for ease.&lt;/p&gt;
&lt;p&gt;Next add some breakpoints in PHPStorm and set it to listen for any debug connections using the listener icon:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/02/Listen-to-debug-connections.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="Listen to debug connections" class="gallery-image" data-flex-basis="546px" data-flex-grow="227" height="90" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/debug-cli-remote-server/Listen-to-debug-connections.png" width="205"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now login to your Remote Server via SSH etc.&lt;/p&gt;
&lt;p&gt;You now need to change settings for Xdebug in either xdebug.ini or php.ini depending on how you installed it. You also need to know the IP of the local machine. This can permanently set in the Network Setting of your VM in Virtual Box, so you will never have to change it. In my example the local machine running PHPStorm is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;192.168.56.1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now edit the ini file that contains your Xdebug settings and set the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_host = 192.168.56.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_connect_back = 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_port = 9000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_handler = dbgp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_mode = req
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_enable = 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.idekey = phpstorm1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Be aware you might have to change the &lt;strong&gt;remote_host&lt;/strong&gt; and the &lt;strong&gt;idekey&lt;/strong&gt; based on your own environment. To better understand what each option does, see &lt;a class="link" href="http://xdebug.org/docs/all_settings" title="Xdebug Settings"
 target="_blank" rel="noopener"
 &gt;Xdebug Settings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Finally, when running the script you must set the following variables:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PHP_IDE_CONFIG=&amp;#34;serverName=dev.example.com&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP_IDE_CONFIG will tell PHPStorm how to map the Remote File Paths to what it sees Locally. Again replace the URL with the Name/Host you set in PHPStorm. &lt;strong&gt;Note:&lt;/strong&gt; You can export this, if your system is only running one site; mine is not.&lt;/p&gt;
&lt;p&gt;You can run this inline with your script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PHP_IDE_CONFIG=&amp;#34;serverName=dev.example.com&amp;#34; ./testscript.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This should send you to PHPStorm where you earlier placed breakpoints.&lt;/p&gt;
&lt;p&gt;Happy Debugging!&lt;/p&gt;</description></item><item><title>Quick and easy setup of and connection to NRPE on Ubuntu</title><link>https://blog-570662.gitlab.io/quick-dirty-setup-nrpe-ubuntu/</link><pubDate>Wed, 06 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/quick-dirty-setup-nrpe-ubuntu/</guid><description>&lt;h2 id="about-nrpe"&gt;About NRPE
&lt;/h2&gt;&lt;p&gt;NRPE (Nagios Remote Plugin Executor) is a useful tool that allows you to execute scripts on remote servers and return the output for ingestion by some form of monitoring software.&lt;/p&gt;
&lt;h2 id="setup"&gt;Setup
&lt;/h2&gt;&lt;p&gt;We currently have our own instance of Icinga running to monitor our servers and have recently started to offer access to it for our clients.&lt;/p&gt;
&lt;p&gt;The majority of our servers (and our clients servers if we set them up) use one variant or another of Ubuntu. This means we can very quickly get our servers connected to a Nagios/Icinga instance.&lt;/p&gt;
&lt;p&gt;First things first we need to install the nrpe server and all the associated plugins&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install nagios-nrpe-server \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins-basic \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins-extra
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next we need to edit the main nrpe config file to be found @ /etc/nagios/nrpe.cfg. What your looking for is the lines&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ALLOWED HOST ADDRESSES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This is an optional comma-delimited list of IP address or hostnames &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# that are allowed to talk to the NRPE daemon.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Note: The daemon only does rudimentary checking of the client&amp;#39;s IP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# address. I would highly recommend adding entries in your /etc/hosts.allow&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# file to allow only the specified host to connect to the port&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# you are running this daemon on.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# NOTE: This option is ignored if NRPE is running under either inetd or xinetd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;allowed_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# COMMAND ARGUMENT PROCESSING&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This option determines whether or not the NRPE daemon will allow clients&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# to specify arguments to commands that are executed. This option only works&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# if the daemon was configured with the --enable-command-args configure script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# option. &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# *** ENABLING THIS OPTION IS A SECURITY RISK! *** &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Read the SECURITY file for information on some of the security implications&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# of enabling this variable.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Values: 0=do not allow arguments, 1=allow command arguments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dont_blame_nrpe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will want to change this to the IP of your Nagios/Icinga instance and set the dont_blame_nrpe value to 1. Feel free to take a look round the rest of the file. Its all quite interesting and generally will documented. Be careful what you change though in case something breaks.&lt;/p&gt;
&lt;p&gt;You will also want to look for some lines that are refererd to as &amp;ldquo;COMMAND DEFINITIONS&amp;rdquo; and look something like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_hda1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hda1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_zombie_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Z&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_total_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can go ahead and comment these out as we will be adding our own definitions shortly. The main reason for removing these is that we will be configuring some specific scripts for our own use later that allow you to configure your requirements and thereshold from within your Nagios/Icinga config.&lt;/p&gt;
&lt;h2 id="configuration-of-monitoring-server"&gt;Configuration of Monitoring Server
&lt;/h2&gt;&lt;p&gt;Once this is complete you can now configure a new &amp;ldquo;check command&amp;rdquo; for use with your nagios/icinga server.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;define command {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_name check_nrpe
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;define command {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_name check_nrpe_command_args
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$ -a $ARG2$
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here you can see that we have set up 2 different check commands. The first is a simple command requiring only one argument of $ARG1$ which would be the name of the command we want to run on the remote server. The second command is almost identical except for the fact it takes a second argument which allows you to input a series of &amp;ldquo;arguments&amp;rdquo; to be passed to the command on your remote server. each argument should be separated by a space.&lt;/p&gt;
&lt;p&gt;Now that you have these you can then configure your hosts and services to make use of it. I would recommend having a trawl through the Nagios/Icinga sites &amp;amp; documentation to find out how to create a config that suits you.&lt;/p&gt;
&lt;h2 id="configuration-of-remote-server"&gt;Configuration of Remote Server
&lt;/h2&gt;&lt;p&gt;Now that we have our monitoring server ready its time to add the command we want to run to the remote server.&lt;/p&gt;
&lt;p&gt;To do this your /etc/nagios/nrpe.cfg shoudl hopefully have a line in it that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include=/etc/nagios/nrpe_local.cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;if it doesn&amp;rsquo;t have a line like that then add it and edit the `/etc/nagios/nrpe_local.cfg` file to look a little like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_apt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_apt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sda1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG3&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_zombie_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Z&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_total_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These are a few simple commands that I tend to use most often. These translate to your &amp;ldquo;check_nrpe&amp;rdquo; commands like so&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$ARG1$ = everything inside the square brackets [ ]&lt;/li&gt;
&lt;li&gt;$ARG2$ = each of the $ARG?$ keys as a single string separated by a space&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s done you should be able restart your nrpe server with `/etc/init.d/nagios-nrpe-server restart`&lt;/p&gt;
&lt;p&gt;It really is that simple. Do bear in mind that because you can pass arbitrary arguments into nrpe this was you could leave yourself vulnerable to a bit of maliciousness so its a good idea to make sure your firewall restricts port 5666 (the default port) to IPs you trust.&lt;/p&gt;</description></item><item><title>Compiling Apache 2.4 on Ubuntu 12.04</title><link>https://blog-570662.gitlab.io/compiling-apache-2-4-ubuntu-12-04/</link><pubDate>Tue, 06 Nov 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/compiling-apache-2-4-ubuntu-12-04/</guid><description>&lt;p&gt;I&amp;rsquo;ve decided that I need to up my game when it comes to webservers. However I&amp;rsquo;m not yet ready to switch to Nginx or one of the other webservers out in the wild as I need something up and running rapidly.&lt;/p&gt;
&lt;p&gt;Granted the numbers are definitely against Apache in a lot of benchmarks but historically I&amp;rsquo;ve always had a good experience and the entry level makes it much more appropriate for me to stick with it.&lt;/p&gt;
&lt;p&gt;However Apache 2.2 is rather long in the tooth, thankfully 2.4 has been out for a while now. The problem I have is that I tend to favour Ubuntu as a platform and there is no sign of a 2.4 version appearing on the horizon anytime soon as they are waiting for it to be implemented upsteam in Debian before including it in Ubuntu.&lt;/p&gt;
&lt;p&gt;Now there are PPAs available out there but im not overly happy using them (especially on production environments) So the only option is to compile.&lt;/p&gt;
&lt;p&gt;First thing is to install all the dependencies we are going to need. Thankfuly ubuntu has a nice and simple way of handling this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get build-dep apache2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can then download the source code and start the compilation.&lt;/p&gt;
&lt;p&gt;So from the root of our new copy of the source we need to run our configure.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./configure --prefix=/usr/local/apache2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-mods-shared=all \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-http \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-deflate \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-expires \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-slotmem-shm \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-headers \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-rewrite \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-balancer \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-http \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-fcgi \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-mime-magic \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-log-debug \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --with-mpm=event
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will notice that I&amp;rsquo;m installing it using the event mpm. Hopefully I&amp;rsquo;ll be covering more about the event mpm in the future.&lt;/p&gt;
&lt;p&gt;Next we need to run make&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make &amp;amp;&amp;amp; make install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that&amp;rsquo;s complete you should be able to run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/local/apache2/bin/apachectl start
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and get the &amp;ldquo;it works&amp;rdquo; message through your webrowser when accessing the server IP.&lt;/p&gt;
&lt;p&gt;Dont forget to configure apache to suit your specific requirements.&lt;/p&gt;
&lt;p&gt;Something that will come up is how to start apache on boot. Seeing as Ubuntu uses Upstart it makes sense to utilise it for controlling apache.&lt;/p&gt;
&lt;p&gt;So in the file `/etc/ini/apache.conf` we need to put&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# apache2 - http server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Apache is a web server that responds to HTTP and HTTPS requests.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Required-Start: $local_fs $remote_fs $network $syslog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Required-Stop: $local_fs $remote_fs $network $syslog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Matt Cockayne &amp;lt;matt@zucchi.co.uk&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Apache 2.4 HTTP Server&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;runlevel&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;runlevel&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="mi"&gt;2345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mkdir&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ssl_scache shouldn&amp;#39;t be here if we&amp;#39;re just starting up.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# (this is bad if there are several apache2 instances running)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/*&lt;/span&gt;&lt;span class="n"&gt;ssl_scache&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Give up if restart occurs 10 times in 30 seconds.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;respawn&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;respawn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;envvars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;envvars&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ULIMIT_MAX_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ulimit -S -n `ulimit -H -n`&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x$ULIMIT_MAX_FILES&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ULIMIT_MAX_FILES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;httpd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="n"&gt;FOREGROUND&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a rather simple upstart script and I will be looking to update it at some point&amp;hellip; but it works&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done you should find that on reboot Apache will start and take advantage of all the management features of upstart including attempting to respawn Apache should it end unexpectedly. You should also be able to then use the following commands to control Apache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to start start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restart apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# check the status of apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl status apache
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I generally tend to avoid using the apachectl script found at /usr/local/apache/bin/apachectl once upstart takes control.&lt;/p&gt;</description></item><item><title>Compiling PHP 5.4 on Ubuntu 12.04</title><link>https://blog-570662.gitlab.io/compiling-php-5-4-ubuntu-12-04/</link><pubDate>Tue, 06 Nov 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/compiling-php-5-4-ubuntu-12-04/</guid><description>&lt;p&gt;So recently I&amp;rsquo;ve been working with PHP 5.4 a LOT. Unfortunately Ubuntu (my main dev environment) is behind the times. So I&amp;rsquo;m resorting to compiling PHP manually.&lt;/p&gt;
&lt;p&gt;Not a daunting as it may first appear. The really tricky part is working out your dependencies and `configure` script.&lt;/p&gt;
&lt;p&gt;Hence the reason for this post as a reminder for myself and others that may want to do a quick compile. (I would recommend that if your compiling for a production/live environment that you make sure you understand what it is your compiling though before just using what&amp;rsquo;s here)&lt;/p&gt;
&lt;p&gt;So where to start. Dependencies first I think&lt;/p&gt;
&lt;p&gt;Ubuntu allows you to install dependencies for building source `apt-get build-deps`. We will use this and install any extras we may need.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxml2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxml2-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libssl-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg-config \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libcurl4-nss-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;enchant \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libenchant-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libjpeg8 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libjpeg8-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpng12-0 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpng12-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libvpx1 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libvpx-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libfreetype6 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libfreetype6-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libt1-5 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libt1-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libgmp10 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libgmp-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libicu48 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libicu-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mcrypt \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libmcrypt4 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libmcrypt-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpspell-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libedit2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libedit-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libsnmp15 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libsnmp-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxslt1.1 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxslt1-dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now the configure&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;apxs2&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apxs&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;openssl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kerberos&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zlib&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;curl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;curlwrappers&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;enchant&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;exif&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ftp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;jpeg&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;vpx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;freetype&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t1lib&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;exif&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ttf&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;jis&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gettext&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gmp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mhash&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;intl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mbstring&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mcrypt&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysqli&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pcntl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pgsql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pgsql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pspell&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;libedit&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;shmop&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;snmp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;soap&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sockets&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sysvmsg&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sysvshm&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;xsl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zip&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pear&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zend&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;maintainer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once these are done then we follow the standard make process. Notice we are also running make test&amp;hellip; very important as it givges more data for the developers to work with.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make &amp;amp;&amp;amp; make test &amp;amp;&amp;amp; make install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The next thing is configuring your php.ini file as the install doesn&amp;rsquo;t have one yet so we copy either the production or development default from the source code to the new conf dir and edit to suit your needs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp {php-source-dir}/php.ini-(development|production) /usr/local/php/conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Thats it. All ready to roll&amp;hellip; almost, this installation is the one I use for use with a webserver so you will want to add the appropriate directives to apache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LoadModule php5_module modules/libphp5.so
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AddHandler php5-script .php
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AddType text/html .php
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Rsync and custom SSH commands</title><link>https://blog-570662.gitlab.io/rsync-custom-ssh-commands/</link><pubDate>Tue, 31 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/rsync-custom-ssh-commands/</guid><description>&lt;p&gt;Rsync is a great tool but can be a pain if you have to jump through hoops to connect via ssh such as connecting via a different port.&lt;/p&gt;
&lt;p&gt;A simple solution is to use the &lt;strong&gt;-e&lt;/strong&gt; flag (also knows as &amp;ndash;rsh=COMMAND). This flag allows you manually define the ssh command to use when connecting&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -e &amp;#39;ssh -p2020&amp;#39; -rav ./* user@server:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Will allow me to connect to a server with SSH listening on port 2020&lt;/p&gt;</description></item><item><title>Nice New Nexus7</title><link>https://blog-570662.gitlab.io/nice-nexus7/</link><pubDate>Thu, 19 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/nice-nexus7/</guid><description>&lt;img src="https://blog-570662.gitlab.io/nice-nexus7/tablet-n7-features-ushome-family.png" alt="Featured image of post Nice New Nexus7" /&gt;&lt;p&gt;This morning I woke up to an email telling me that my Nexus7 that I had ordered 3 weeks ago was&amp;hellip; &amp;ldquo;out for delivery&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I couldn&amp;rsquo;t contain my excitement. I sat patiently waiting by my door. Finally 11 o&amp;rsquo;clock rolls around and there is a knock. I&amp;rsquo;m handed a brown parcel and hand over the obligatory signature. I close the door behind me and carefully place the box on the desk. I contemplate teasing myself and seeing how long I can hold out before opening it.&lt;/p&gt;
&lt;p&gt;That lasted about 20 seconds!!!!&lt;/p&gt;
&lt;p&gt;In fact&amp;hellip; this video says it all&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://www.youtube.com/watch?v=Xijcwbg8CGQ" target="_blank" rel="noopener"
 &gt;http://www.youtube.com/watch?v=Xijcwbg8CGQ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not gonna bore you with how awesome it is (and it is awesome). I will however point out a few obvious foibles with it though (not that they would have ever stopped me from buying it).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No way to expand storage&lt;/li&gt;
&lt;li&gt;Lack of Flash (may not seem important, but until everyone else catches up there is loads of content I cant use i.e. BBC iPlayer)&lt;/li&gt;
&lt;li&gt;A number of apps (games specifically) that I have run on my phone are not yet supported&lt;/li&gt;
&lt;li&gt;Google Now feels a little clunky at times and struggles with some of the regional British accents&lt;/li&gt;
&lt;li&gt;MTP doesn&amp;rsquo;t appear to work out of the box with my Linux OS (I&amp;rsquo;m sure this will be remedied soon)&lt;/li&gt;
&lt;li&gt;No obvious way to directly access the front facing camera (easily remedied with an app from &lt;a class="link" href="https://play.google.com/store/apps/details?id=com.modaco.cameralauncher" title="Camera Launcher"
 target="_blank" rel="noopener"
 &gt;MoDaCo&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Registering custom view helpers in ZF2</title><link>https://blog-570662.gitlab.io/registering-custom-view-helpers-zf2/</link><pubDate>Wed, 18 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/registering-custom-view-helpers-zf2/</guid><description>&lt;p&gt;If you want to register custom view helpers with a module you can do so by using the service location built into the Skeleton Application and creating a module config that looks something like.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;return array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;view_helpers&amp;#39; =&amp;gt; array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;invokables&amp;#39; =&amp;gt; array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // generic view helpers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;truncate&amp;#39; =&amp;gt; &amp;#39;Zucchi\View\Helper\Truncate&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // form based view helpers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapForm&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapForm&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapRow&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapRow&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapCollection&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapCollection&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Bootstrapping ZF2 Forms</title><link>https://blog-570662.gitlab.io/bootstrapping-zf2-forms/</link><pubDate>Tue, 17 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/bootstrapping-zf2-forms/</guid><description>&lt;p&gt;So&amp;hellip;&lt;/p&gt;
&lt;p&gt;With the release of beta 5 for Zend Framework 2 I thought it time for me to tidy up and fix a few modules I created back at beta 3.&lt;/p&gt;
&lt;p&gt;Now I&amp;rsquo;m a big fan of Twitter Bootstrap CSS framework as I&amp;rsquo;m sure a lot of other people are as well. Seeing that the Zend Skeleton Application comes with bootstrap already included it was easy enough to set up my forms using the old ZF Forms found in ZF1.&lt;/p&gt;
&lt;p&gt;However a brand spanking new Forms component has been rolled out with ZF2. The long and the short of this new component meant that I had the opportunity to hand roll a new way of making my forms work with Twitter Bootstrap.&lt;/p&gt;
&lt;p&gt;So, a little tinkering, a quick &lt;a class="link" href="https://github.com/zendframework/zf2/pull/1893" target="_blank" rel="noopener"
 &gt;pull request&lt;/a&gt; to ZF2 to allow the definition of arbitrary options and I came up with some useful View Helpers that can be dropped into a project and used.&lt;/p&gt;
&lt;p&gt;You can find them at &lt;a class="link" href="https://github.com/zucchi/Zucchi/tree/master/src/Zucchi/Form/View/Helper" title="https://github.com/zucchi/Zucchi"
 target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/Zucchi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So how to use them. Lets start by creating a new form (we&amp;rsquo;ll keep it simple for now)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;MyForm&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;myform&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;price&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;required&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;required&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;placeholder&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0.99&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;label&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;help&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;block&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;content&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;The price you wish to use&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;prepend&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;append&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;¢&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;actions&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;form-actions&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Save&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;btn btn-primary&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;inline&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;btn&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;inline&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll notice that I have highlighted some lines. Thanks to the ability to set arbitrary options we can define a &amp;ldquo;bootstrap&amp;rdquo; option which we can then use to allow us to pass data into our new bootstrap view helpers. You can also see that I have added a save and reset button to a collection. I&amp;rsquo;ll explain that later.&lt;/p&gt;
&lt;p&gt;So what next&amp;hellip; Rather than go into the mechanics of how to work with forms I&amp;rsquo;ll refer you to &lt;a class="link" href="http://zend-framework-2-doc.readthedocs.org/en/latest/modules/zend.form.intro.html" title="the ZF documentation"
 target="_blank" rel="noopener"
 &gt;the ZF documentation&lt;/a&gt; and this excellent &lt;a class="link" href="http://www.michaelgallego.fr/blog/?p=190" title="New Zend\Form features explained"
 target="_blank" rel="noopener"
 &gt;blog post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We then pick up by looking at your view, and the helpers I have created.&lt;/p&gt;
&lt;h2 id="bootstrapformform-formstyle"&gt;BootstrapForm($form, $formStyle)
&lt;/h2&gt;&lt;p&gt;One of the few things I miss from the ZF1 implementation of Forms is the self rendering aspect! So what did I decide to do? That&amp;rsquo;s right I created a view helper to render everything in one command.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;$this-&amp;gt;bootstrapForm()&lt;/code&gt; takes two parameters. The first is quite obviously the form. The second is the style of form. This is directly related to the form types that can be found &lt;a class="link" href="http://twitter.github.com/bootstrap/base-css.html#forms" title="here"
 target="_blank" rel="noopener"
 &gt;http://twitter.github.com/bootstrap/base-css.html#forms&lt;/a&gt;. You can use any of &amp;lsquo;vertical&amp;rsquo;, &amp;lsquo;inline&amp;rsquo;, &amp;lsquo;search&amp;rsquo; &amp;amp; &amp;lsquo;horizontal&amp;rsquo;. If you dont specify a formStyle then it will default to &amp;lsquo;vertical&amp;rsquo;&lt;/p&gt;
&lt;p&gt;Caveat: This helper will then iterate through all of the associated elements and render them first. Only after the direct elements have been generated will it then move onto Collections or Fieldsets (as soon as I work out how I&amp;rsquo;ll fix this).&lt;/p&gt;
&lt;h2 id="bootstraprowelement-formstyle"&gt;BootstrapRow($element, $formStyle)
&lt;/h2&gt;&lt;p&gt;This is a straightforward modification of the &lt;code&gt;FormRow&lt;/code&gt; helper that come bundled with the new component.&lt;/p&gt;
&lt;p&gt;We have a few differences now though. We have a second parameter as with the &lt;code&gt;BootstrapForm&lt;/code&gt; view helper and the output is generated using sprintf and a set of templates that mimic the structures of the different form styles from bootstrap.&lt;/p&gt;
&lt;p&gt;This helper can be used by itself to generate an element row and is used by the &lt;code&gt;BootstrapForm&lt;/code&gt; helper&lt;/p&gt;
&lt;p&gt;We can also now take advantage of the &amp;ldquo;bootstrap&amp;rdquo; options we set earlier.&lt;/p&gt;
&lt;h3 id="bootstrap-options"&gt;Bootstrap Options
&lt;/h3&gt;&lt;p&gt;style&lt;/p&gt;
&lt;p&gt;The style of form element to use regardless of what style may be passed into the view helper (you can see an example of this in the buttons from the MyForm example above)&lt;/p&gt;
&lt;p&gt;help&lt;/p&gt;
&lt;p&gt;This works in the same way as &amp;ldquo;description&amp;rdquo; did from ZF1 but allows you to define it either as a string or an array with the keys &amp;ldquo;style&amp;rdquo; for either &amp;lsquo;inline&amp;rsquo; or &amp;lsquo;block&amp;rsquo; and &amp;ldquo;Content&amp;rdquo; which should be self explainatory&lt;/p&gt;
&lt;p&gt;prepend&lt;/p&gt;
&lt;p&gt;Takes advantage of Bootstraps ability to prepend blocks to an input field. This can be defined as a single string, or an array of strings to allow you to add multiple blocks should you want to&lt;/p&gt;
&lt;p&gt;prepend&lt;/p&gt;
&lt;p&gt;Takes advantage of Bootstraps ability to append blocks to an input field. This can be defined as a single string, or an array of strings to allow you to add multiple blocks should you want to&lt;/p&gt;
&lt;p&gt;These options get evaluated and spat out from the new &lt;code&gt;renderBootstrapOptions()&lt;/code&gt; method as part of the &amp;ldquo;render&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="bootstrapcollectionelement-style-wrap"&gt;BootstrapCollection($element, $style, $wrap)
&lt;/h2&gt;&lt;p&gt;Again this is a direct rip off of the &lt;code&gt;FormCollection&lt;/code&gt; helper found in the ZF2 Form component witha few modifications. The main difference is that is makes use of the &lt;code&gt;BootstrapRow&lt;/code&gt; helper and has methods and properties to allow the setting of the form style to use.&lt;/p&gt;
&lt;p&gt;You can see from the &lt;code&gt;MyForm&lt;/code&gt; example above that we set a &lt;code&gt;Collection&lt;/code&gt; called &amp;lsquo;actions&amp;rsquo;. This is a pretty standard way of grouping elements together. You can also see that we set a class for the &lt;code&gt;Collection&lt;/code&gt; which may look familiar to those that have used Twitter Bootstrap for a while.&lt;/p&gt;
&lt;p&gt;What our helper will then do is wrap the buttons in a div with the appropriate class attached. If you were to define a &lt;code&gt;label&lt;/code&gt; for the Collection/Fieldset You would then also find that the fieldset and legend tags are also spat out with our &lt;code&gt;&amp;lt;div class=&amp;quot;form-actions&amp;quot;&amp;gt;&lt;/code&gt; sandwiched between them and the elements.&lt;/p&gt;
&lt;h3 id="result"&gt;Result
&lt;/h3&gt;&lt;p&gt;So what we now get when we use &lt;code&gt;MyForm&lt;/code&gt; with out helpers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$this-&amp;gt;bootstrapForm($form, &amp;#39;horizontal&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should now look something like this&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2012/07/bootstrap-result1.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="results of bootstrap helper" class="gallery-image" data-flex-basis="760px" data-flex-grow="316" data-title-escaped="bootstrap-result" height="159" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/bootstrapping-zf2-forms/bootstrap-result1.png" title="bootstrap-result" width="504"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="how-you-can-use-it"&gt;How you can use it
&lt;/h3&gt;&lt;p&gt;As of right now you can get the library from its repo on github @ &lt;a class="link" href="https://github.com/zucchi/Zucchi" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/Zucchi&lt;/a&gt; and can be found on &lt;a class="link" href="http://packagist.org/packages/zucchi/zucchi" title="zucchi/zucchi"
 target="_blank" rel="noopener"
 &gt;packagist&lt;/a&gt; for use with composer&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit:&lt;/strong&gt; The bootstrap stuff has moved to a new location as a separate ZF2 module. you can find it @ &lt;a class="link" href="https://github.com/zucchi/ZucchiBootstrap" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/ZucchiBootstrap&lt;/a&gt; or @ &lt;a class="link" href="http://packagist.org/packages/zucchi/bootstrap" title="zucchi/bootstrap"
 target="_blank" rel="noopener"
 &gt;packagist&lt;/a&gt; for use with composer&lt;/p&gt;</description></item><item><title>Loaded Testing</title><link>https://blog-570662.gitlab.io/loaded-testing/</link><pubDate>Sat, 30 Jun 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/loaded-testing/</guid><description>&lt;p&gt;I recently had to do some load testing for a site recently that would allow me to test in excess of 100k requests in a 60 second period&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://jmeter.apache.org/" target="_blank" rel="noopener"
 &gt;&lt;img alt="JMeter" class="gallery-image" data-flex-basis="520px" data-flex-grow="216" data-title-escaped="jmeter-logo" height="102" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/loaded-testing/jmeter-logo.jpg" title="jmeter-logo" width="221"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I decided to do some testing using JMeter as it seemed like a suitable tool for doing what I needed and I had used it for some simpler testing in the past.&lt;/p&gt;
&lt;p&gt;After a little fumbling around I managed to get a test plan designed that would simulate 10k users actually navigating the site and adding to a cart etc, with a number of various interactions. It wasnt perfect but it would correctly simulate over 100k requests.&lt;/p&gt;
&lt;p&gt;So feeling quite pleased with myself I started the test from my laptop. Now I&amp;rsquo;m not a big gamer, I&amp;rsquo;m known to play a little World or Warcraft from time to time but that&amp;rsquo;s about it. So when it comes to computing power i tend to opt for battery life over sheer grunt.&lt;/p&gt;
&lt;p&gt;Suffice to say, my laptop fell flat on its face, and if it hadn&amp;rsquo;t it turns out that the connection I was using just wasn&amp;rsquo;t up to the task of handling that much traffic adequately.&lt;/p&gt;
&lt;p&gt;So plan B&amp;hellip;&lt;/p&gt;
&lt;p&gt;I quickly fired up the largest AWS instance available and got a copy of jmeter installed. A little tinkering with my test plan and some googling on how to run jmeter without a gui and a quick&lt;/p&gt;
&lt;p&gt;&lt;code&gt;./jmeter -n -t test-plan.jmx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and it appeared to be running.&lt;/p&gt;
&lt;p&gt;(Please bear in mind that I&amp;rsquo;m being overly kind&amp;hellip; it took a LOT of tinkering and twice as much Googling to work out how to get the test results out so i could actually get some idea of WTF was happening during the test)&lt;/p&gt;
&lt;p&gt;So&amp;hellip; client &amp;ldquo;happy&amp;rdquo;&amp;hellip; I decided to go and find a better way to do my load testing in the future.&lt;/p&gt;
&lt;p&gt;Sticking with JMeter I managed to find this gem of a page&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://jmeter.apache.org/usermanual/remote-test.html" title="Remote Testing"
 target="_blank" rel="noopener"
 &gt;http://jmeter.apache.org/usermanual/remote-test.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;tl;dr &amp;gt; use your local install of jmeter to trigger tests to run on one or more remote &amp;ldquo;nodes&amp;rdquo; and then have all the results sent to your local install.&lt;/p&gt;
&lt;p&gt;So I set to work!&lt;/p&gt;
&lt;h2 id="building-a-node"&gt;&lt;strong&gt;Building a Node&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;First I need to set up an AWS instance that we can use and duplicate so I can quickly build a cluster of nodes on demand. I&amp;rsquo;m a big fan of Ubuntu so I spin up a micro instance of 12.04 server. Next I shell into the instance and install the default Java runtime from apt&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt-get install openjdk-7-jre&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Yes I know there are other more appropriate runtimes, but i dont really care&amp;hellip; i just need it to work and it does.&lt;/p&gt;
&lt;p&gt;next I grab a copy of the latest stable from &lt;a class="link" href="http://jmeter.apache.org/download_jmeter.cgi" title="Download Apache JMeter"
 target="_blank" rel="noopener"
 &gt;http://jmeter.apache.org/download_jmeter.cgi&lt;/a&gt; and un-tar it to &lt;code&gt;/usr/local/jmeter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(N.B. JMeter is available through the apt but I had issues with that version and you need to make sure that both your local version and all the nodes run the same version of jmeter)&lt;/p&gt;
&lt;p&gt;We can now test that the install is working running &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; and you should get some output that looks similar to&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Created remote object: UnicastServerRef [liveRef: [endpoint:[10.???.???.???:38939](local),objID:[46522b57:138381f1023:-7fff, 2635011707874933136]]]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which tells us that the server is running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BUT&lt;/strong&gt; unfortunately its not going to work just yet. Because we are using Amazons EC2 we are going to relying on their NAT for routing. Out of the box JMeter just wont work properly.&lt;/p&gt;
&lt;p&gt;However there is something we can do to combat this. We can set the parameter &lt;code&gt;RMI_HOST_DEF&lt;/code&gt; that the &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; script will include in starting the server.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="n"&gt;RMI_HOST_DEF&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="n"&gt;Djava&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rmi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wget&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ll explain what we are doing here. Amazon have been quite clever by providing a &lt;a class="link" href="http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html" target="_blank" rel="noopener"
 &gt;meta-data endpoint&lt;/a&gt; that you can poll from within your instance to get key pieces of data&amp;hellip; Including the public dns record.&lt;/p&gt;
&lt;p&gt;We can use this endpoint and using wget pipe that into the &lt;code&gt;RMI_HOST_DEF&lt;/code&gt; param (ensuring that we prepend &lt;code&gt;-D&lt;/code&gt;) and then export that so it becomes available to the &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; script.&lt;/p&gt;
&lt;p&gt;Now to get the server to start on boot.&lt;/p&gt;
&lt;p&gt;a quick upstart script should solve this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Upstart script to initialise jmeter-server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JMeter Server&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Dev in Charge &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="n"&gt;networking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;stopping&lt;/span&gt; &lt;span class="n"&gt;networking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;stopping&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# get the current public DNS record&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="n"&gt;RMI_HOST_DEF&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="n"&gt;Djava&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rmi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wget&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# start jmeter in server mde&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jmeter&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jmeter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;saving this to &lt;code&gt;/etc/init/jmeter-server.conf&lt;/code&gt; will mean that it will auto-start jmeter-server on boot and allow you to manually control the process using &lt;code&gt;start jmeter-server&lt;/code&gt; and &lt;code&gt;stop jmeter-server&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and thats it&amp;hellip; instance configured&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://aws.amazon.com/" target="_blank" rel="noopener"
 &gt;&lt;img alt="Powered by AWS" class="gallery-image" data-flex-basis="590px" data-flex-grow="245" data-title-escaped="AWS_Logo_PoweredBy_300px" height="122" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/loaded-testing/AWS_Logo_PoweredBy_300px.png" title="AWS_Logo_PoweredBy_300px" width="300"&gt;&lt;/a&gt;All you need to do now is save the instance as an AMI and you have an on-demand image for spinning up a cluster of remote JMeter servers for you to play with.&lt;/p&gt;
&lt;h2 id="configuring-your-local-installation"&gt;Configuring your local installation
&lt;/h2&gt;&lt;p&gt;Now that the server side is working we need to configure our local installation to allow it to connect.&lt;/p&gt;
&lt;p&gt;First things first however, make sure you are using the same version of JMeter as you are running on the server.&lt;/p&gt;
&lt;p&gt;We need to edit the &lt;code&gt;jmeter.properties&lt;/code&gt; file that can be found in the bin folder of the installtion you downloaded. Look for the parameter &lt;code&gt;remote_hosts&lt;/code&gt; This needs to be set with the public dns of the remote server(s) your connecting to. for example&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;remote_hosts=ec2-176-34-164-170.eu-west-1.compute.amazonaws.com,ec2-123-34-456-789.eu-west-1.compute.amazonaws.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Thats your local version configured. You will now be able to tell your local version to run tests on any or all of your specified remotes.&lt;/p&gt;
&lt;p&gt;However if your like me you work behind a router/firewall. If so this isnt the end of the story. When you send a test plan to a remote from your local install it will also send the IP address of your local machine for it to send the results back to. JMeter does this by looking up where your current hostname resolves to. In my circumstance it resolved to &lt;code&gt;127.0.1.1&lt;/code&gt;. The reason it did this is down to the fact my systems host file had the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;127.0.1.1 devincharge.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To resolve this I had to change it to my external IP address&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;89.345.871.79 devincharge.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And set up port forwarding from my router to my local machine for all ports from 1024 to 65535. Now, you can if you want use specific ports so you dont have to port forward everything from your router, but i&amp;rsquo;ll leave that for you to lookup as there are plenty resources on how to do this for you to google and I&amp;rsquo;ve waffled on for far too long already.&lt;/p&gt;
&lt;p&gt;Happy testing&lt;/p&gt;</description></item></channel></rss>