<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>blog::weyl.io</title>
    <link>https://weyl.io/</link>
    <description>Chris Weyl&#39;s technical blog</description>
    <generator>Hugo 0.155.3 &amp; FixIt v0.4.0-alpha.3-20251225101113-8ffb9a95</generator>
    <language>en-us</language>
    <managingEditor>chris@weyl.io (Chris Weyl)</managingEditor>
    <webMaster>chris@weyl.io (Chris Weyl)</webMaster>
    <lastBuildDate>Sun, 28 Dec 2025 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://weyl.io/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Task and Taskfiles</title>
      <link>https://weyl.io/2025/12/taking-make-to-task/</link>
      <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2025/12/taking-make-to-task/</guid>
      <category domain="https://weyl.io/categories/dev-tooling/">Dev Tooling</category>
      <description>&lt;p&gt;If you&amp;rsquo;ve been around a project for any length of time, you&amp;rsquo;re familiar with&#xA;the need for automation &amp;ndash; specifically, something to keep track of all the&#xA;little tasks that need to be done over and over again.  These tasks are&#xA;typically at least somewhat deterministic, don&amp;rsquo;t vary from run to run, and are&#xA;run by more than one person.&lt;/p&gt;&#xA;&lt;p&gt;The classic solution here is to use &lt;code&gt;make&lt;/code&gt; and a &lt;code&gt;Makefile&lt;/code&gt;.  For older&#xA;codebases where you&amp;rsquo;re only worried about building C or C++ code, this is very&#xA;acceptable &amp;ndash; it&amp;rsquo;s what &lt;code&gt;make&lt;/code&gt; was designed for, after all.  &lt;code&gt;make&lt;/code&gt; is nearly&#xA;universally available on Unix-like systems, and it&amp;rsquo;s relatively simple to use&#xA;for basic tasks.  The challenge comes in when you need to execute&#xA;non-deterministic tasks; things like running tests, linting code, bringing&#xA;test environments up and down, and so on.  &lt;code&gt;make&lt;/code&gt; can handle these tasks but&#xA;it&amp;rsquo;s not always the best tool for the job. &lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;task-a-modern-job-runner&#34;&gt;&lt;span&gt;&lt;code&gt;task&lt;/code&gt;, a modern job-runner&lt;/span&gt;&#xA;  &lt;a href=&#34;#task-a-modern-job-runner&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;alert alert-note&#34;&gt;&lt;p class=&#34;alert-title&#34;&gt;&lt;svg class=&#34;icon&#34; xmlns=&#34;http://www.w3.org/2000/svg&#34; viewBox=&#34;0 0 16 16&#34; width=&#34;16&#34; height=&#34;16&#34;&gt;&lt;path d=&#34;M0 8a8 8 0 1116 0A8 8 0 010 8zm8-6.5a6.5 6.5.0 100 13 6.5 6.5.0 000-13zM6.5 7.75A.75.75.0 017.25 7h1a.75.75.0 01.75.75v2.75h.25a.75.75.0 010 1.5h-2a.75.75.0 010-1.5h.25v-2h-.25a.75.75.0 01-.75-.75zM8 6a1 1 0 110-2 1 1 0 010 2z&#34;/&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;This is likely to be the first in an irregular series on &lt;code&gt;task&lt;/code&gt;.&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Roughly 8 months ago, a colleague and friend introduced me to the &lt;a href=&#34;https://taskfile.dev/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;code&gt;task&lt;/code&gt;&#xA;program&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; and the &lt;code&gt;Taskfile&lt;/code&gt;s it uses.  &lt;code&gt;task&lt;/code&gt; is a general purpose task&#xA;runner; a system designed to trivially enable writing, documenting, and&#xA;running tasks.  More YAML, yes, but that&amp;rsquo;s hardly unusual these days, but more&#xA;importantly &amp;ndash; no arcane incantations, no mystical recipes.  Just a&#xA;well-designed YAML schema and straight-forward embedded bash.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;jobs-targets-and-dependencies&#34;&gt;&lt;span&gt;jobs, targets and dependencies&lt;/span&gt;&#xA;  &lt;a href=&#34;#jobs-targets-and-dependencies&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Both &lt;code&gt;make&lt;/code&gt; and &lt;code&gt;task&lt;/code&gt; define each job as a &amp;ldquo;target&amp;rdquo; (though &lt;code&gt;task&lt;/code&gt; prefers&#xA;to call them&amp;hellip; &amp;ldquo;tasks&amp;rdquo;).  Both allow for the chaining of tasks, either&#xA;implicitly or explicitly.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m going to set aside how &lt;code&gt;make&lt;/code&gt; dependencies work for the moment &amp;ndash; there&#xA;being more books, papers, and articles on that topic than I&amp;rsquo;m willing to even&#xA;hazard a guess at enumerating &amp;ndash; and take a look at how &lt;code&gt;task&lt;/code&gt; handles it.&lt;/p&gt;&#xA;&lt;p&gt;Generally speaking, task dependencies can be broken out into:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Does &lt;em&gt;this&lt;/em&gt; task need to be run?&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Did the source change; or&lt;/li&gt;&#xA;&lt;li&gt;Does this generic test fail?&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;This other task needs to be run before/after/during.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The former of which can be thought of as &amp;ldquo;self dependency&amp;rdquo;, while the latter&#xA;is more typically what we think of as a dependency.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m not going to regurgitate the documentation here, as, well, if you&amp;rsquo;re&#xA;reading this you&amp;rsquo;re not likely to appreciate that very much, but I do want to&#xA;give a couple examples that illustrate how powerful this approach is.&lt;/p&gt;&#xA;&lt;p&gt;Both of these forms may be combined or used in dependent tasks to support&#xA;useful workflows.  For example, let&amp;rsquo;s say you have a golang project, one that&#xA;requires certain services to be spun up beforehand before you can attempt to&#xA;run/test the built binary.  Note that we have a couple different dependency&#xA;types here: does the binary need to be (re)built?  Is the test service up?&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;---&#xA;version: &amp;#34;3&amp;#34;&#xA;&#xA;tasks:&#xA;&#xA;  # executes &amp;#34;cmds&amp;#34; only if task determines that &amp;#34;thingie&amp;#34; needs to be&#xA;  # rebuilt from the sources listed&#xA;  build:&#xA;    desc: &amp;#39;Build it!&amp;#39;&#xA;    sources:&#xA;      - &amp;#39;**/*.go&amp;#39;&#xA;      - go.mod&#xA;      - go.sum&#xA;    generates:&#xA;      - thingie&#xA;    cmds:&#xA;      - cmd: go build -o thingie&#xA;&#xA;  service-up:&#xA;    desc: Bring up a service we need for... reasons&#xA;    status:&#xA;      - systemctl is-active ollama.service&#xA;    cmds:&#xA;      - cmd: systemctl start ollama.service&#xA;      - task: service-pull&#xA;&#xA;  service-pull:&#xA;    desc: Something we need to do and may want to do independently&#xA;    deps:&#xA;      - service-up&#xA;    vars:&#xA;      MODEL: &amp;#39;hf.co/ibm-granite/granite-4.0-h-micro-gguf:latest&amp;#39;&#xA;    status:&#xA;      - &amp;#39;ollama list | grep -q {{.MODEL}}&amp;#39;&#xA;    cmds:&#xA;      - cmd: &amp;#39;ollama pull {{.MODEL}}&amp;#39;&#xA;&#xA;  run:&#xA;    desc: Run the test/app/whatever&#xA;    deps:&#xA;      - build&#xA;      - service-up&#xA;    cmds:&#xA;      - cmd: ./thingie ...&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the above, note how:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;code&gt;build&lt;/code&gt; will only execute if any of the sources change (much as &lt;code&gt;make&lt;/code&gt;&#xA;does).&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;code&gt;service-up&lt;/code&gt; only attempts to start the service if it isn&amp;rsquo;t already&#xA;started.  (Agreed, with &lt;code&gt;systemd&lt;/code&gt; this isn&amp;rsquo;t a huge savings, but imagine if&#xA;the service is running inside a container launched by &lt;code&gt;podman&lt;/code&gt;, etc)&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;code&gt;service-up&lt;/code&gt; declares a &amp;ldquo;run-time&amp;rdquo; dependency on &lt;code&gt;service-pull&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;code&gt;service-pull&lt;/code&gt; only attempts to pull the model if it is not already present&#xA;on the system.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The net result of this is a set of tasks flexible enough that we&amp;rsquo;re able to&#xA;run each step individually (if we so chose), but with expressive enough&#xA;dependency information that we&amp;rsquo;re not going to have tasks failing due to a&#xA;dependency not being built / started / fetched / etc.&lt;/p&gt;&#xA;&lt;p&gt;Enjoy! &lt;i class=&#34;fas fa-grin&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;For an excellent example of both the power and the pitfalls of&#xA;using &lt;code&gt;make&lt;/code&gt; for non-build tasks, see the &lt;code&gt;Makefile&lt;/code&gt; generated by the&#xA;&lt;a href=&#34;https://github.com/kubernetes-sigs/kubebuilder&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;code&gt;kubebuilder&lt;/code&gt; project&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>Linting Thoughts</title>
      <link>https://weyl.io/2024/10/linting-thoughts/</link>
      <pubDate>Tue, 29 Oct 2024 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2024/10/linting-thoughts/</guid>
      <category domain="https://weyl.io/categories/tradecraft/">Tradecraft</category>
      <description>&lt;img src=&#34;https://weyl.io/2024/10/linting-thoughts/linty_daleks_preview.png&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;Linting is an one of those things that can be weirdly controversial.  I say&#xA;weirdly as I cannot quite understand the objections to it &amp;ndash; it&amp;rsquo;s not like&#xA;we&amp;rsquo;re all perfect typists, after all.  Generally the objections range from &amp;ldquo;My&#xA;editor does it for me&amp;rdquo; to &amp;ldquo;I forget to run them before pushing!&amp;rdquo; to my&#xA;favorite, &amp;ldquo;They always fail in CI!&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;None of those are legitimate reasons to avoid linting.  We call it &amp;ldquo;linting&amp;rdquo;&#xA;because it&amp;rsquo;s like the lint on your clothes: small, annoying, seems to&#xA;spontaneously self-incarnate, and &lt;em&gt;is trivial to remove&lt;/em&gt;.  Linting is one of&#xA;the things computers excel at.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;what-is-linting&#34;&gt;&lt;span&gt;What is Linting?&lt;/span&gt;&#xA;  &lt;a href=&#34;#what-is-linting&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Simply put: &lt;strong&gt;Linting is any automated process that enforces a deterministic,&#xA;agreed-on set of coding, format, style, or other technical standards.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Tip&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;The content you&amp;rsquo;re linting can have a great impact on the tools you use to&#xA;lint, and what you can lint.  For example, a statically typed language like&#xA;Go can be linted in ways that a dynamically typed language like Perl cannot.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Linting &lt;em&gt;is&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Spell-checking&lt;/li&gt;&#xA;&lt;li&gt;Validating data file formats&lt;/li&gt;&#xA;&lt;li&gt;Validating end-of-file and other platform issues&lt;/li&gt;&#xA;&lt;li&gt;Static code checkers / analysis&lt;/li&gt;&#xA;&lt;li&gt;Code style checks&lt;/li&gt;&#xA;&lt;li&gt;Ensuring license headers are applied everywhere&lt;/li&gt;&#xA;&lt;li&gt;Other fully automated checks for formatting, style, etc, that can be run&#xA;without human intervention&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Linting &lt;em&gt;is not&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Code reviews&lt;/li&gt;&#xA;&lt;li&gt;Manual testing&lt;/li&gt;&#xA;&lt;li&gt;User acceptance testing&lt;/li&gt;&#xA;&lt;li&gt;Anything requiring human intervention&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Linting &lt;em&gt;may&lt;/em&gt; be:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Running unit tests&lt;/li&gt;&#xA;&lt;li&gt;Building binaries (to validate it actually builds, not deployment)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&amp;hellip;that&amp;rsquo;s really up to you.&lt;/p&gt;&#xA;&lt;p&gt;Linters may also suggest fixes that can then be evaluated and applied, if&#xA;appropriate.  This is not necessary, but is very convenient.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;how-do-we-lint&#34;&gt;&lt;span&gt;How do we lint?&lt;/span&gt;&#xA;  &lt;a href=&#34;#how-do-we-lint&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;All effective linting implementations I&amp;rsquo;ve seen have the following characteristics:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;They use a linting framework&lt;/li&gt;&#xA;&lt;li&gt;They can be run locally&lt;/li&gt;&#xA;&lt;li&gt;They are run and enforced in CI/CD&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;linting-frameworks&#34;&gt;&lt;span&gt;Linting Frameworks&lt;/span&gt;&#xA;  &lt;a href=&#34;#linting-frameworks&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s certainly possible to &amp;ldquo;roll your own&amp;rdquo; approach here, but why?  While&#xA;there aren&amp;rsquo;t a huge number of linting frameworks, all you really need is one&#xA;good, general-purpose one.  (Which is why there aren&amp;rsquo;t a huge number of them.)&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://pre-commit.com/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Pre-commit&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; is an excellent example of a linting&#xA;framework.  It is Open Source, is widely used, has a large number of pre-built&#xA;hooks, and is trivially extensible.  Additionally, it is well-documented,&#xA;well-supported, and actively maintained.  It&amp;rsquo;s name implies a certain&#xA;workflow: it is designed to be run before each commit.  However, this is not&#xA;the case.  It is a general-purpose linting framework with built-in support for&#xA;Git hooks, but it can be run at any time.&lt;/p&gt;&#xA;&lt;div class=&#34;gh-repo-card-container single&#34;&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/pre-commit/pre-commit&#34; class=&#34;repo-url&#34; title=&#34;pre-commit/pre-commit&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;pre-commit/&lt;/span&gt;&lt;span&gt;pre-commit&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;A framework for managing and maintaining multi-language pre-commit hooks.&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/pre-commit/pre-commit/stargazers&#34; title=&#34;14953 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;15k&lt;/a&gt;&lt;a href=&#34;https://github.com/pre-commit/pre-commit/forks&#34; title=&#34;927 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;927&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/pre-commit/pre-commit-hooks&#34; class=&#34;repo-url&#34; title=&#34;pre-commit/pre-commit-hooks&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;pre-commit/&lt;/span&gt;&lt;span&gt;pre-commit-hooks&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;Some out-of-the-box hooks for pre-commit&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/pre-commit/pre-commit-hooks/stargazers&#34; title=&#34;6365 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;6.4k&lt;/a&gt;&lt;a href=&#34;https://github.com/pre-commit/pre-commit-hooks/forks&#34; title=&#34;777 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;777&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;h4 class=&#34;heading-element&#34; id=&#34;pre-commit-linters-hooks&#34;&gt;&lt;span&gt;Pre-commit linters (&amp;ldquo;hooks&amp;rdquo;)&lt;/span&gt;&#xA;  &lt;a href=&#34;#pre-commit-linters-hooks&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h4&gt;&lt;div class=&#34;details admonition note open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-pencil-alt&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Note&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;Pre-commit &amp;ldquo;hooks&amp;rdquo; are the individual linters that are run by the pre-commit&#xA;framework; they should not be confused with Git hooks.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve included the base, general &lt;code&gt;pre-commit-hooks&lt;/code&gt; repository above.  Note&#xA;that this is not the only repository of hooks available &amp;ndash; there are many&#xA;others, including some that are specific to a particular language or tool.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s a few examples of pre-commit hooks that I&amp;rsquo;ve found useful:&lt;/p&gt;&#xA;&lt;div class=&#34;gh-repo-card-container single&#34;&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/TekWizely/pre-commit-golang&#34; class=&#34;repo-url&#34; title=&#34;TekWizely/pre-commit-golang&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;TekWizely/&lt;/span&gt;&lt;span&gt;pre-commit-golang&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;Pre-commit hooks for Golang with support for monorepos, the ability to pass arguments and environment variables to all hooks, and the ability to invoke custom go tools.&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #89e051;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Shell&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/TekWizely/pre-commit-golang/stargazers&#34; title=&#34;357 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;357&lt;/a&gt;&lt;a href=&#34;https://github.com/TekWizely/pre-commit-golang/forks&#34; title=&#34;41 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;41&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/adrienverge/yamllint&#34; class=&#34;repo-url&#34; title=&#34;adrienverge/yamllint&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;adrienverge/&lt;/span&gt;&lt;span&gt;yamllint&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;A linter for YAML files.&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/adrienverge/yamllint/stargazers&#34; title=&#34;3315 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;3.3k&lt;/a&gt;&lt;a href=&#34;https://github.com/adrienverge/yamllint/forks&#34; title=&#34;307 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;307&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/syntaqx/git-hooks&#34; class=&#34;repo-url&#34; title=&#34;syntaqx/git-hooks&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;syntaqx/&lt;/span&gt;&lt;span&gt;git-hooks&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;A collection of git hooks for use with pre-commit&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #89e051;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Shell&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/syntaqx/git-hooks/stargazers&#34; title=&#34;34 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;34&lt;/a&gt;&lt;a href=&#34;https://github.com/syntaqx/git-hooks/forks&#34; title=&#34;16 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;16&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/hadolint/hadolint&#34; class=&#34;repo-url&#34; title=&#34;hadolint/hadolint&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;hadolint/&lt;/span&gt;&lt;span&gt;hadolint&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;Dockerfile linter, validate inline bash, written in Haskell&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #5e5086;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Haskell&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/hadolint/hadolint/stargazers&#34; title=&#34;11953 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;12k&lt;/a&gt;&lt;a href=&#34;https://github.com/hadolint/hadolint/forks&#34; title=&#34;487 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;487&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/python-jsonschema/check-jsonschema&#34; class=&#34;repo-url&#34; title=&#34;python-jsonschema/check-jsonschema&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;python-jsonschema/&lt;/span&gt;&lt;span&gt;check-jsonschema&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;A CLI and set of pre-commit hooks for jsonschema validation with built-in support for GitHub Workflows, Renovate, Azure Pipelines, and more!&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/python-jsonschema/check-jsonschema/stargazers&#34; title=&#34;299 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;299&lt;/a&gt;&lt;a href=&#34;https://github.com/python-jsonschema/check-jsonschema/forks&#34; title=&#34;57 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;57&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/IBM/detect-secrets&#34; class=&#34;repo-url&#34; title=&#34;IBM/detect-secrets&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;IBM/&lt;/span&gt;&lt;span&gt;detect-secrets&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-fork-from&#34;&gt;Fork from &lt;a href=&#34;https://github.com/Yelp/detect-secrets&#34; target=&#34;_blank&#34;&gt;Yelp/detect-secrets&lt;/a&gt;&lt;/p&gt;&lt;p class=&#34;repo-desc&#34;&gt;An enterprise friendly way of detecting and preventing secrets in code.&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/IBM/detect-secrets/stargazers&#34; title=&#34;84 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;84&lt;/a&gt;&lt;a href=&#34;https://github.com/IBM/detect-secrets/forks&#34; title=&#34;53 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;53&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/Lucas-C/pre-commit-hooks&#34; class=&#34;repo-url&#34; title=&#34;Lucas-C/pre-commit-hooks&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;Lucas-C/&lt;/span&gt;&lt;span&gt;pre-commit-hooks&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;git pre-commit hooks&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #3572A5;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Python&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/Lucas-C/pre-commit-hooks/stargazers&#34; title=&#34;151 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;151&lt;/a&gt;&lt;a href=&#34;https://github.com/Lucas-C/pre-commit-hooks/forks&#34; title=&#34;53 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;53&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  &lt;div class=&#34;gh-repo-card&#34;&gt;&#xA;      &lt;div class=&#34;repo-card-content&#34;&gt;&#xA;        &lt;div class=&#34;repo-name&#34;&gt;&#xA;          &lt;svg aria-hidden=&#34;true&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo mr-1 color-fg-muted&#34;&gt;&#xA;            &lt;path d=&#34;M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z&#34;&gt;&lt;/path&gt;&#xA;          &lt;/svg&gt;&#xA;          &lt;a href=&#34;https://github.com/terraform-docs/terraform-docs&#34; class=&#34;repo-url&#34; title=&#34;terraform-docs/terraform-docs&#34; target=&#34;_blank&#34;&gt;&#xA;            &lt;span&gt;terraform-docs/&lt;/span&gt;&lt;span&gt;terraform-docs&lt;/span&gt;&#xA;          &lt;/a&gt;&lt;span class=&#34;repo-visibility&#34; data-archived=&#34;false&#34;&gt;Public&lt;/span&gt;&#xA;        &lt;/div&gt;&lt;p class=&#34;repo-desc&#34;&gt;Generate documentation from Terraform modules in various output formats&lt;/p&gt;&#xA;        &lt;p class=&#34;repo-statistics&#34;&gt;&lt;span class=&#34;repo-lang&#34;&gt;&#xA;              &lt;span class=&#34;repo-lang-color&#34; style=&#34;background-color: #00ADD8;&#34;&gt;&lt;/span&gt;&#xA;              &lt;span itemprop=&#34;programmingLanguage&#34;&gt;Go&lt;/span&gt;&#xA;            &lt;/span&gt;&lt;a href=&#34;https://github.com/terraform-docs/terraform-docs/stargazers&#34; title=&#34;4701 stars&#34; class=&#34;repo-stars&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;stars&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-star&#34;&gt;&#xA;                &lt;path d=&#34;M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;4.7k&lt;/a&gt;&lt;a href=&#34;https://github.com/terraform-docs/terraform-docs/forks&#34; title=&#34;587 forks&#34; class=&#34;repo-forks&#34; target=&#34;_blank&#34;&gt;&#xA;              &lt;svg aria-label=&#34;forks&#34; role=&#34;img&#34; height=&#34;16&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; data-view-component=&#34;true&#34; class=&#34;octicon octicon-repo-forked&#34;&gt;&#xA;                &lt;path d=&#34;M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z&#34;&gt;&lt;/path&gt;&#xA;              &lt;/svg&gt;587&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Pre-commit hooks are also &lt;a href=&#34;https://pre-commit.com/#new-hooks&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;surprisingly easy to create&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;when-do-we-lint&#34;&gt;&lt;span&gt;When do we lint?&lt;/span&gt;&#xA;  &lt;a href=&#34;#when-do-we-lint&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Tip&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;Generally speaking, at a minimum one should lint before submitting a changeset&#xA;for review (that is, creating a pull request).  This is a minimum level of&#xA;courtesy to your peers.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;I always lint before pushing &amp;ndash; setting up a &lt;a href=&#34;https://git-scm.com/docs/githooks#_pre_push&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;pre-push&#xA;hook&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; tends to make this&#xA;impossible to forget.  Others have other preferences, and that&amp;rsquo;s fine: running&#xA;the linters in CI/CD ensures that they cannot be forgotten.  (&amp;hellip;and that the&#xA;reviewer does not need to enforce them.)&lt;/p&gt;&#xA;&lt;p&gt;A successful linting workflow tends to run like this:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Local&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Lint at some point before pushing&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;CI&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Lint as the first pipeline job on every push&lt;/li&gt;&#xA;&lt;li&gt;Fail the entire pipeline early if linting fails&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;linting-locally&#34;&gt;&lt;span&gt;Linting Locally&lt;/span&gt;&#xA;  &lt;a href=&#34;#linting-locally&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Running locally is both critical and not essential.  It&amp;rsquo;s critical in that&#xA;if you push a change that fails linting, your changeset will fail CI.&#xA;Similarly, it is not essential as you can always push a change, wait for CI to&#xA;fail and then fix it.  Your call.&lt;/p&gt;&#xA;&lt;p&gt;As Pre-commit&amp;rsquo;s name implies, it was originally designed to be run before you&#xA;make a commit &amp;ndash; e.g. as a pre-commit hook.  However, with modern Git&#xA;workflows this can be quite time-consuming.  (Imagine having to wait for your&#xA;linters to run every time you create a &lt;code&gt;fixup!&lt;/code&gt; commit &amp;ndash; it gets old fast.)&#xA;A happy balance between &amp;ldquo;every time&amp;rdquo; and &amp;ldquo;never&amp;rdquo; is to run it before you push,&#xA;typically as a pre-push hook.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;pre-commit install --install-hooks --hook-type pre-push&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Regardless of the tool used to lint, leveraging a pre-push hook helps ensure&#xA;that you catch any linting errors before anyone else sees them.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;lint-in-ci&#34;&gt;&lt;span&gt;Lint in CI&lt;/span&gt;&#xA;  &lt;a href=&#34;#lint-in-ci&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;div class=&#34;details admonition warning open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-exclamation-triangle&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Warning&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;I cannot emphasize this enough: &lt;strong&gt;Enforce your lint rules in CI.&lt;/strong&gt;  People&#xA;didn&amp;rsquo;t stop littering because it was the right thing to do.  They stopped&#xA;because they were fined for it.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;It is &lt;em&gt;imperative&lt;/em&gt; that you lint in CI.  Not linting in CI defeats the whole&#xA;purpose of linting.  We lint to catch the (typically) trivial mistakes we may&#xA;make and we run CI to automate testing for mistakes.  If we believed every&#xA;changeset to be perfect, we wouldn&amp;rsquo;t have CI in the first place.&lt;/p&gt;&#xA;&lt;p&gt;If you find your CI/CD pipelines failing frequently due to linting errors, you&#xA;may want to examine some of your own practices.  (Like, say, setting up a&#xA;pre-push hook?)&lt;/p&gt;&#xA;&lt;p&gt;If you do not enforce your linting rules in CI, you&amp;rsquo;re effectively forcing&#xA;other people to do it for you.  This is not only rude, but it&amp;rsquo;s also a waste&#xA;of time and resources.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;what-do-we-lint&#34;&gt;&lt;span&gt;What do we lint?&lt;/span&gt;&#xA;  &lt;a href=&#34;#what-do-we-lint&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Tip&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;Linting is not just for code.  It can be used to validate data files, check&#xA;your spelling, and even ensure that your documentation is up-to-date.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Simple answer?  &lt;strong&gt;We lint everything we can.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;If you can lint it, you should.  Linting is much like brushing your teeth or&#xA;washing your hands &amp;ndash; it&amp;rsquo;s a simple, effective way to prevent problems.  Also,&#xA;it&amp;rsquo;s &lt;em&gt;really&lt;/em&gt; gross, obvious to even the most casual of observers, and just&#xA;downright lazy&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; if you don&amp;rsquo;t.&lt;/p&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re using pre-commit, there are a huge number of pre-built hooks you can&#xA;use as well as a large number of third-party hooks.  It is also easy to write&#xA;your own, should the need arise.&lt;/p&gt;&#xA;&lt;p&gt;Important things to lint include:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;platform-specific line and file endings are consistent&lt;/li&gt;&#xA;&lt;li&gt;data files (e.g. JSON, YAML, TOML) are not malformed&lt;/li&gt;&#xA;&lt;li&gt;code is legal and styled consistently&lt;/li&gt;&#xA;&lt;li&gt;the project is buildable (e.g. it is &amp;ldquo;complete&amp;rdquo; and coherent)&lt;/li&gt;&#xA;&lt;li&gt;secrets have not been committed&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;you-have-to-start-somewhere&#34;&gt;&lt;span&gt;You have to start somewhere&lt;/span&gt;&#xA;  &lt;a href=&#34;#you-have-to-start-somewhere&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Tip&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;Take the Boy Scout approach to linting: Every time you find lint, leave the&#xA;project a little cleaner (and the linter a little smarter) than you found it.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;It&amp;rsquo;s not often to possible to know all the things you need to lint from the&#xA;start.  Much as with testing, you&amp;rsquo;ll find additional things to lint for as&#xA;your project moves forward &amp;ndash; especially as additional people, perhaps using&#xA;different operating systems, editors, or tooling, begin to contribute.  I&amp;rsquo;ve&#xA;found it useful to treat linting like other testing processes:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Lint (test) all the obvious things right off the bat; and&lt;/li&gt;&#xA;&lt;li&gt;Add additional linting as you find issues.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;For example, it&amp;rsquo;s fairly obvious in a Go project that running &lt;code&gt;gofmt&lt;/code&gt; is a&#xA;good idea.  It is perhaps less obvious that you should lint your Markdown&#xA;&lt;code&gt;README.md&lt;/code&gt; to ensure it renders correctly &amp;ndash; until someone pushes one that&#xA;does not.  Similarly, it&amp;rsquo;s fairly obvious that one should lint YAML to ensure&#xA;it&amp;rsquo;s valid, but less obvious that you should enforce a consistent style of&#xA;indenting lists.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;finally&#34;&gt;&lt;span&gt;Finally&amp;hellip;&lt;/span&gt;&#xA;  &lt;a href=&#34;#finally&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Linting is a simple, effective way to ensure that your project is clean and&#xA;consistent.  We created computers to help us with automated, repetitive,&#xA;deterministic tasks like this &amp;ndash; tasks that humans have a tendency to forget&#xA;or overlook.  Automated, enforced linting is a simple and effective way to&#xA;instantly raise and ensure the overall hygiene and quality of any project.&lt;/p&gt;&#xA;&lt;p&gt;Just do it!  Linting is only ever a big deal &lt;em&gt;when it is not done.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;I mean, come on.  This isn&amp;rsquo;t the good type of lazy &amp;ndash; this is the bad&#xA;type that &lt;a href=&#34;https://thethreevirtues.com/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;imperils your Hubris&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;</description>
    </item>
    <item>
      <title>terraform-provider-gitlabci: Register GitLab CI Runners</title>
      <link>https://weyl.io/2022/01/terraform-provider-gitlabci/</link>
      <pubDate>Fri, 14 Jan 2022 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2022/01/terraform-provider-gitlabci/</guid>
      <category domain="https://weyl.io/categories/gitlab/">GitLab</category>
      <category domain="https://weyl.io/categories/infrastructure-as-code/">Infrastructure as Code</category>
      <description>&lt;img src=&#34;https://weyl.io/2022/01/terraform-provider-gitlabci/gitlab-plus-terraform.png&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;I&amp;rsquo;m a huge fan of terraform, so when I needed to build out cloud&#xA;infrastructure for GitLab CI/CD it was the first thing I reached for.  The&#xA;native &lt;code&gt;terraform-provider-gitlab&lt;/code&gt; was very useful, but left out one critical&#xA;detail: it was not possible to &lt;a href=&#34;https://docs.gitlab.com/runner/register/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;register a runner&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Ouch. &amp;#x1f4a2;&lt;/p&gt;&#xA;&lt;p&gt;This left a rather annoyingly awkward gap in my terraform configurations, as&#xA;I&amp;rsquo;d need to provision a runner token &lt;em&gt;outside&lt;/em&gt; of terraform.  I messed around&#xA;with this, coming up with a couple&amp;hellip; interesting approaches, but ultimately I&#xA;realized that the only proper solution to this (read: that wasn&amp;rsquo;t just a giant&#xA;hack) would require writing a provider.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;terraform-provider-gitlabci&#34;&gt;&lt;span&gt;&lt;code&gt;terraform-provider-gitlabci&lt;/code&gt;&lt;/span&gt;&#xA;  &lt;a href=&#34;#terraform-provider-gitlabci&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Recently I&amp;rsquo;ve had some free time, so I cleaned it up a bit and published it.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://registry.terraform.io/providers/rsrchboy/gitlabci/latest/docs&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;terraform registry (docs, etc)&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://gitlab.com/rsrchboy/terraform-provider-gitlabci&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;i class=&#34;fab fa-gitlab&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&amp;nbsp;source hosted at GitLab&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;a-quick-example&#34;&gt;&lt;span&gt;A quick example&lt;/span&gt;&#xA;  &lt;a href=&#34;#a-quick-example&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Documentation and the like can be found over at the &lt;a href=&#34;https://registry.terraform.io/providers/rsrchboy/gitlabci/latest/docs&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;terraform registry&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;,&#xA;but here&amp;rsquo;s a quick example with only a minimum of hand-wavey:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;terraform {&#xA;  required_providers {&#xA;    gitlabci = {&#xA;      source = &amp;#34;registry.terraform.io/rsrchboy/gitlabci&amp;#34;&#xA;    }&#xA;    gitlab = {&#xA;      source = &amp;#34;registry.terraform.io/gitlabhq/gitlab&amp;#34;&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;provider &amp;#34;gitlabci&amp;#34; { }&#xA;provider &amp;#34;gitlab&amp;#34;   { }&#xA;&#xA;data &amp;#34;gitlab_project&amp;#34; &amp;#34;this&amp;#34; {&#xA;  id = &amp;#34;rsrchboy/terraform-provider-gitlabci&amp;#34;&#xA;}&#xA;&#xA;resource &amp;#34;gitlabci_runner_token&amp;#34; &amp;#34;this&amp;#34; {&#xA;  registration_token = data.gitlab_project.this.runners_token&#xA;  locked             = true&#xA;  tags = [&#xA;    &amp;#34;jinx&amp;#34;,&#xA;    &amp;#34;powder&amp;#34;,&#xA;    &amp;#34;cupcake&amp;#34;,&#xA;  ]&#xA;}&#xA;&#xA;output &amp;#34;token&amp;#34; {&#xA;  sensitive = true&#xA;  value     = gitlabci_runner_token.this.token&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note how, using both the &lt;code&gt;gitlab&lt;/code&gt; and &lt;code&gt;gitlabci&lt;/code&gt; providers we can now register&#xA;GitLab runners.  The example shows us using a registration token obtained from&#xA;a project data source, but &lt;code&gt;terraform-provider-gitlabci&lt;/code&gt; doesn&amp;rsquo;t care if it&amp;rsquo;s&#xA;a project, group, or even instance registration token.  Additionally, while&#xA;the &lt;code&gt;gitlab&lt;/code&gt; provider does require API access, the &lt;code&gt;gitlabci&lt;/code&gt; provider only&#xA;requires a valid registration token.&lt;/p&gt;&#xA;&lt;p&gt;Enjoy! &lt;i class=&#34;fas fa-grin&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Docker container network interface ordering</title>
      <link>https://weyl.io/2022/01/docker-container-network-interface-ordering/</link>
      <pubDate>Sat, 08 Jan 2022 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2022/01/docker-container-network-interface-ordering/</guid>
      <category domain="https://weyl.io/categories/random-tidbits/">Random Tidbits</category>
      <description>&lt;img src=&#34;https://weyl.io/2022/01/docker-container-network-interface-ordering/plugging-patch-cable.jpg&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;Lately I&amp;rsquo;ve found myself routinely attaching Docker-based containers to&#xA;multiple networks.  This has led to a couple&amp;hellip; interesting surprises.&lt;/p&gt;&#xA;&lt;p&gt;It doesn&amp;rsquo;t seem to be well documented (AFAIK), so here&amp;rsquo;s what I&amp;rsquo;ve learned.&#xA;This isn&amp;rsquo;t a big serious how-to post, just something that irked me. &amp;#x1f604;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;tooling-a-little-background&#34;&gt;&lt;span&gt;Tooling (a little background)&lt;/span&gt;&#xA;  &lt;a href=&#34;#tooling-a-little-background&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition note&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-pencil-alt&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Observed behavior&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;As this is observed behavior rather than documented (AFAICT, at any rate), it&#xA;may change without warning.&lt;/p&gt;&#xA;&lt;p&gt;Oh well.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;For the purposes of this article, let&amp;rsquo;s say we&amp;rsquo;re using the following tools:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/docker&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;docker&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/docker/compose&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;docker-compose&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;network-name-determines-ordering&#34;&gt;&lt;span&gt;Network name determines ordering&lt;/span&gt;&#xA;  &lt;a href=&#34;#network-name-determines-ordering&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;What&amp;rsquo;s the actual network name?&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;When using Compose, there are really two network &amp;ldquo;names&amp;rdquo; for each network.&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The name specified in the Compose configuration; and&lt;/li&gt;&#xA;&lt;li&gt;The name Compose uses when creating the Docker network.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;In a compose configuration, we always use #1 to refer to a network; however&#xA;behind the scenes this will be different from the name of the network as known&#xA;to Docker.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Networks are attached in alphabetical order.  Internal networks are attached&#xA;last, though still in alphabetical order.&lt;/p&gt;&#xA;&lt;p&gt;Networks appear to be attached in alphabetical order, further segregated by&#xA;the internal status.  Not-internal networks are attached to the container&#xA;before internal networks.&lt;/p&gt;&#xA;&lt;p&gt;Let&amp;rsquo;s say we have a container with three networks attached:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;version: &amp;#34;3.9&amp;#34;&#xA;&#xA;services:&#xA;  container1:&#xA;  image: docker.io/containous/whoami&#xA;    networks:&#xA;      backend: {}&#xA;      frontend: {}&#xA;      apples: {}&#xA;      oranges: {}&#xA;&#xA;networks:&#xA;  backend: {}&#xA;  frontend: {}&#xA;  apples:&#xA;    internal: true&#xA;  oranges:&#xA;    name: lemons&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Let&amp;rsquo;s say this has a project name of &lt;code&gt;services&lt;/code&gt;.  When &lt;code&gt;container1&lt;/code&gt; is&#xA;launched, it will see a number of interfaces:&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Interface&lt;/th&gt;&#xA;          &lt;th&gt;Network (Compose)&lt;/th&gt;&#xA;          &lt;th&gt;Network (Docker)&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;eth0&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;oranges&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;lemons&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;eth1&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;backend&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;services_backend&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;eth2&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;frontend&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;services_frontend&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;eth3&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;apples&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;services_apples&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Why?  &lt;code&gt;apples&lt;/code&gt; is internal, so it ends up at the end of the list.  &lt;code&gt;oranges&lt;/code&gt;&#xA;has an actual name of &lt;code&gt;lemons&lt;/code&gt;, so it ends up before the other networks with&#xA;a &lt;code&gt;services_&lt;/code&gt; prefix.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;default-route-is-always-through-eth0&#34;&gt;&lt;span&gt;Default route is always through &lt;code&gt;eth0&lt;/code&gt;&lt;/span&gt;&#xA;  &lt;a href=&#34;#default-route-is-always-through-eth0&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;If you want a specific network to be the default route, you&amp;rsquo;re going to need&#xA;to make sure it&amp;rsquo;s first &amp;ndash; alphabetically.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;routing-and-macvlan-interfaces&#34;&gt;&lt;span&gt;Routing and macvlan interfaces&lt;/span&gt;&#xA;  &lt;a href=&#34;#routing-and-macvlan-interfaces&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;This is more of a corner case.  If you&amp;rsquo;re attaching containers directly to a&#xA;vlan/network using, say, &lt;code&gt;macvlan&lt;/code&gt;, and that network ends up being your&#xA;default route (&lt;code&gt;app_vlan&lt;/code&gt;, anyone?), then all traffic for networks you&amp;rsquo;re not&#xA;directly connected to will be routed over this vlan.&lt;/p&gt;&#xA;&lt;p&gt;This may or may not matter, but if you have the (reasonable) expectation that&#xA;non-local traffic will route through and be masqueraded by the host, this can&#xA;be surprising.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Using a Metadata Proxy to Limit AWS/IAM Access with GitLab CI</title>
      <link>https://weyl.io/2020/10/using-a-metadata-proxy-for-secure-iam-in-gitlab-ci/</link>
      <pubDate>Mon, 05 Oct 2020 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2020/10/using-a-metadata-proxy-for-secure-iam-in-gitlab-ci/</guid>
      <category domain="https://weyl.io/categories/gitlab/">GitLab</category>
      <description>&lt;img src=&#34;https://weyl.io/2020/10/using-a-metadata-proxy-for-secure-iam-in-gitlab-ci/phone-operator.jpg&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;The &lt;code&gt;gitlab-runner&lt;/code&gt; agent is very flexible, with &lt;a href=&#34;https://docs.gitlab.com/runner/executors/README.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;multiple executors&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&#xA;to handle most situations.  Similarly, AWS IAM allows one to use &amp;ldquo;instance&#xA;profiles&amp;rdquo; with EC2 instances, obviating the need for static, long-lived&#xA;credentials.  In the situation where one is running &lt;code&gt;gitlab-runner&lt;/code&gt; on an EC2&#xA;instance, this presents us with a couple interesting challenges &amp;ndash; and&#xA;opportunities.&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;How does one prevent CI jobs from being able to obtain credentials against&#xA;the instance&amp;rsquo;s profile role?&lt;/li&gt;&#xA;&lt;li&gt;How does one allow certain CI jobs to assume credentials through the metadata&#xA;service without allowing &lt;em&gt;all&lt;/em&gt; CI jobs to assume those credentials?&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;criteria&#34;&gt;&lt;span&gt;Criteria&lt;/span&gt;&#xA;  &lt;a href=&#34;#criteria&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition info open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-circle-info&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Relevant Configuration&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;This isn&amp;rsquo;t going to be a comprehensive, step-by-step guide that can be&#xA;followed without any external knowledge or resources.  Rather, we&amp;rsquo;re going to&#xA;focus on what one needs to know in order to implement this solution, however&#xA;you&amp;rsquo;re currently provisioning CI agents.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;For our purposes, we want:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;The &lt;code&gt;gitlab-runner&lt;/code&gt; agent to run on an EC2 instance, with one or more&#xA;runners configured.&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#xA;&lt;li&gt;All configured runners should be using the &lt;a href=&#34;https://docs.gitlab.com/runner/executors/docker.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Docker executor&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Jobs to run, by default, without access to the EC2 instance&amp;rsquo;s profile&#xA;credentials.&lt;/li&gt;&#xA;&lt;li&gt;Certain jobs to assume a specific role transparently through the EC2&#xA;metadata service by virtue of what runner picks them up.&lt;/li&gt;&#xA;&lt;li&gt;Reasonable security:&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Jobs can&amp;rsquo;t just specify an arbitrary role to assume&lt;/li&gt;&#xA;&lt;li&gt;No hardcoded, static, or long-lived credentials&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;only-short-term-transient-credentials&#34;&gt;&lt;span&gt;Only short-term, transient credentials&lt;/span&gt;&#xA;  &lt;a href=&#34;#only-short-term-transient-credentials&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;It&amp;rsquo;s worth emphasizing this:  no hardcoded, static, or long-lived credentials.&#xA;Sure, it&amp;rsquo;s easy to generate an IAM user and plunk its keys in (hopefully)&#xA;&lt;a href=&#34;https://docs.gitlab.com/ee/ci/variables/README.html#protect-a-custom-variable&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;protected environment variables&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;, but then you have to worry about key&#xA;rotation, audits, etc, in the way one doesn&amp;rsquo;t with transient credentials.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;executor-implies-methodology&#34;&gt;&lt;span&gt;Executor implies methodology&lt;/span&gt;&#xA;  &lt;a href=&#34;#executor-implies-methodology&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;For our purposes, we&amp;rsquo;re going to solution this using the agent&amp;rsquo;s &lt;a href=&#34;https://docs.gitlab.com/runner/executors/docker.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;docker executor&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&#xA;Other executors will have different solutions (e.g. kubernetes&#xA;has tools like &lt;a href=&#34;https://github.com/uswitch/kiam&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;kiam&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;).&lt;/p&gt;&#xA;&lt;p&gt;However, for fun let&amp;rsquo;s cheat a bit and do a quick-and-fuzzy run-through of a&#xA;couple of the other executors.&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition note&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-pencil-alt&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;docker+machine executor&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;This is largely like the plain &lt;code&gt;docker&lt;/code&gt; executor, except that as EC2 instances&#xA;will be spun up to handle jobs you can take a detour around anything complex&#xA;by simply telling the agent to associate specific instance profiles with those&#xA;new instances, e.g.:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[[runners]]&#xA;  [runners.machine]&#xA;    MachineOptions = [&#xA;        &amp;#34;amazonEC2-iam-instance-profile=everything-except-the-thing&amp;#34;,&#xA;        ...,&#xA;      ]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The instance running the &lt;code&gt;gitlab-runner&lt;/code&gt; agent does not need to be associated&#xA;with the same profile &amp;ndash; but the agent does need to be able to&#xA;&lt;code&gt;EC2:AssociateIamInstanceProfile&lt;/code&gt; and &lt;code&gt;iam:PassRole&lt;/code&gt; the relevant resources.&lt;/p&gt;&#xA;&lt;p&gt;The downside is that you&amp;rsquo;ll have to have multiple runners configured&#xA;if you want to be able to allow different jobs to assume different roles.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;div class=&#34;details admonition note&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-pencil-alt&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;kubernetes executor&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;The &lt;code&gt;kubernetes&lt;/code&gt; executor is going to be a bit trickier, and, as ever,&#xA;TMTOWTDI[^tmtowtdi].  Depending on what you&amp;rsquo;re doing, any of the following&#xA;might work for you:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Launch nodes with the different profiles and use constraints to pick and&#xA;choose which job pods end up running on them.&lt;/li&gt;&#xA;&lt;li&gt;Use a solution like &lt;a href=&#34;https://github.com/uswitch/kiam&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;kiam&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#xA;&lt;/ol&gt;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;brute-force&#34;&gt;&lt;span&gt;Brute force&lt;/span&gt;&#xA;  &lt;a href=&#34;#brute-force&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Ever a popular option, you can just brute-force block container (job) access&#xA;to the EC2 metadata service by firewalling it off, e.g.:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;iptables -t nat -I PREROUTING \&#xA;    --destination 169.254.169.254 --protocol tcp --dport 80 \&#xA;    -i docker&amp;#43; -j REJECT&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you just want to block all access from jobs, this is a good way to do it.&lt;/p&gt;&#xA;&lt;p&gt;This approach is contraindicated if you want to be able to allow &lt;em&gt;some&lt;/em&gt;&#xA;containers to access the metadata service, or to allow them to retrieve&#xA;credentials of some (semi) arbitrary role.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;ec2-metadata-proxy&#34;&gt;&lt;span&gt;EC2 metadata proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#ec2-metadata-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;A more flexible solution can be found by using a metadata proxy.  This sort of&#xA;service should be a benevolent man-in-the-middle: able to access the actual&#xA;EC2 metadata service for its own credentials, able to inspect containers&#xA;making requests to determine what role (if any) they should be assuming, and&#xA;able to assume those roles and pass tokens back to jobs without those jobs&#xA;being any the wiser about it.&lt;/p&gt;&#xA;&lt;p&gt;For our purposes, we will use&#xA;&lt;a href=&#34;https://github.com/jippi/go-metadataproxy&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;go-metadataproxy&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;, which&#xA;will handle:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;EC2 metadata requests made by processes in containers (e.g. CI&#xA;jobs);&lt;/li&gt;&#xA;&lt;li&gt;Sourcing its own credentials from the actual EC2 metadata service;&lt;/li&gt;&#xA;&lt;li&gt;Inspecting containers for the IAM role that should be assumed (via the&#xA;&lt;code&gt;IAM_ROLE&lt;/code&gt; environment variable);&lt;/li&gt;&#xA;&lt;li&gt;Blocking direct access to the EC2 metadata service; and&lt;/li&gt;&#xA;&lt;li&gt;Assuming the correct role and providing STS tokens transparently to the&#xA;contained process.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;The authentication flow will look something like this:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;sequenceDiagram&#xA;    autonumber&#xA;    participant mdp as metadataproxy&#xA;    participant docker&#xA;    participant job as CI job&#xA;    job-&amp;gt;&amp;gt;mdp: client attempts to request credentials from EC2&#xA;    mdp--&amp;gt;&amp;gt;docker: inspect job container&#xA;    docker--&amp;gt;&amp;gt;mdp: &amp;#34;IAM_ROLE&amp;#34; is &amp;#34;foobar&amp;#34;&#xA;    mdp--&amp;gt;&amp;gt;mdp: STS tokens for role &amp;#34;foobar&amp;#34;&#xA;    mdp-&amp;gt;&amp;gt;job: STS tokens for assumed role &amp;#34;foobar&amp;#34; returned&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This also means that the instance profile role must be able to assume the&#xA;individual roles we want to allow jobs to assume, and the trust policy of the&#xA;individual roles must allow the instance profile role to assume them.&lt;/p&gt;&#xA;&lt;p&gt;In short:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;The instance profile&amp;rsquo;s IAM role policy should only permit certain roles to&#xA;be assumed, either by ARN or some sensible condition (tagged in a certain&#xA;way, etc).&lt;/li&gt;&#xA;&lt;li&gt;Roles in the account, in general, should not blindly trust any principal in&#xA;the account to assume them.&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;configuring-the-ci-agent-correctly&#34;&gt;&lt;span&gt;Configuring the CI agent correctly&lt;/span&gt;&#xA;  &lt;a href=&#34;#configuring-the-ci-agent-correctly&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;div class=&#34;details admonition tip open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-regular fa-lightbulb&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Take care when registering the runner&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;We&amp;rsquo;re not going to cover it here, but take care when &lt;a href=&#34;https://docs.gitlab.com/runner/register/index.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;registering the&#xA;runner&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.  Under this&#xA;approach, &lt;strong&gt;judiciously restricting access to the runner is a critical part of&#xA;controlling what jobs may run with elevated IAM authority&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Keep a couple things in mind:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Registering runners is cheap; better to have more runners for more granular&#xA;security than allow projects / pipelines with no need for access to use&#xA;them.&lt;/li&gt;&#xA;&lt;li&gt;Runners can be registered at the project, group, or (unless you&amp;rsquo;re on&#xA;gitlab.com) the instance level; register them as precisely as your&#xA;requirements allow.&lt;/li&gt;&#xA;&lt;li&gt;Runner access can be further restricted and combined with project/group&#xA;access by allowing them to &lt;a href=&#34;https://docs.gitlab.com/ee/ci/runners/#prevent-runners-from-revealing-sensitive-information&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;run against protected refs only&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;,&#xA;and then &lt;a href=&#34;https://docs.gitlab.com/ee/user/project/protected_branches.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;restricting who can push/merge to protected branches&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; (including&#xA;&lt;a href=&#34;https://docs.gitlab.com/ee/user/project/protected_tags.html&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;protected tags&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;) to trusted individuals.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;div class=&#34;details admonition warning open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-exclamation-triangle&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;Always set IAM_ROLE in the runner configuration&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;Anything that allows a pipeline author to control what role the proxy assumes&#xA;is a security&amp;hellip; concern.  In this context, &lt;code&gt;IAM_ROLE&lt;/code&gt; can be set on the&#xA;container in one of several ways (in order of precedence):&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Through the runner configuration;&lt;/li&gt;&#xA;&lt;li&gt;By the pipeline author; or&lt;/li&gt;&#xA;&lt;li&gt;By the creator of the image.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&lt;strong&gt;Unless you intend to allow the pipeline author to specify the role to&#xA;assume, it is recommended that &lt;code&gt;IAM_ROLE&lt;/code&gt; always be set in the runner&#xA;configuration file, &lt;code&gt;config.toml&lt;/code&gt;.&lt;/strong&gt;  If you don&amp;rsquo;t want any role to be&#xA;assumed, great, set the variable to a blank value.&lt;/p&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;&lt;code&gt;go-metadataproxy&lt;/code&gt; discovers the role to assume by interrogating the docker&#xA;daemon, inspecting the container of the process seeking credentials from the&#xA;EC2 metadata service.  It does this by looking for the value of the &lt;code&gt;IAM_ROLE&lt;/code&gt;&#xA;environment set on the container.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;IAM_ROLE&lt;/code&gt; must be set on the container itself.  While&#xA;&lt;a href=&#34;https://docs.gitlab.com/runner/configuration/advanced-configuration.html#restrict-allowed_images-to-private-registry&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;whitelisting&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&#xA;the list of allowed images isn&amp;rsquo;t a terrible idea, the safest and most reliable&#xA;way of controlling this as the administrator of the runner is to simply set&#xA;the environment variable as part of the runner configuration.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[[runners]]&#xA;  environment = [&#xA;    &amp;#34;IAM_ROLE=some-role-name-or-arn&amp;#34;,&#xA;    ...,&#xA;  ]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This also means that we&amp;rsquo;re going to want a &lt;em&gt;runner configuration per IAM&#xA;role&lt;/em&gt;.  (Not terribly surprising, I would hope.)&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;running-the-metadata-proxy&#34;&gt;&lt;span&gt;Running the metadata proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#running-the-metadata-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;This is reasonably straight-forward, in two parts.  There are a number of ways&#xA;to run it, but as we&amp;rsquo;re doing this in a docker environment anyways, why not&#xA;let it handle all the messy bits for us?&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;$ git clone https://github.com/jippi/go-metadataproxy.git&#xA;$ cd go-metadataproxy&#xA;$ docker build -t local/go-metadataproxy:latest .&#xA;$ docker run \&#xA;    --detach \&#xA;    --restart=always \&#xA;    --net=host \&#xA;    --name=metadataproxy \&#xA;    -v /var/run/docker.sock:/var/run/docker.sock \&#xA;    -e AWS_REGION=us-west-2 \&#xA;    -e ENABLE_PROMETHEUS=1 \&#xA;    local/go-metadataproxy:latest&lt;/code&gt;&lt;/pre&gt;&lt;h1 class=&#34;heading-element&#34; id=&#34;using-the-metadata-proxy&#34;&gt;&lt;span&gt;Using the metadata proxy&lt;/span&gt;&#xA;  &lt;a href=&#34;#using-the-metadata-proxy&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h1&gt;&lt;p&gt;To use the proxy, the containers must be able to reach it in the same way they&#xA;would reach the actual EC2 metadata endpoint.  We need to prevent requests to&#xA;the metadata endpoint from reaching the actual endpoint, and instead be&#xA;transparently redirected to the proxy.  (That is, we&amp;rsquo;re going to play&#xA;Faythe&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a href=&#34;#fn:4&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt; here)&lt;/p&gt;&#xA;&lt;p&gt;To &amp;ldquo;hijack&amp;rdquo; container requests to the EC2 metadata service, a little iptables&#xA;magic is in order.  This is well described in &lt;a href=&#34;https://github.com/jippi/go-metadataproxy#routing-container-traffic-to-go-metadataproxy&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;the project&amp;rsquo;s&#xA;README&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&#xA;I&amp;rsquo;m including it here as well for completeness&amp;rsquo; sake, and with one small&#xA;change: instead of redirecting connections off of &lt;code&gt;docker0&lt;/code&gt;, we reconnect any&#xA;off of &lt;code&gt;docker+&lt;/code&gt;.  (If you&amp;rsquo;re using the runner&amp;rsquo;s &lt;a href=&#34;https://gitlab.com/gitlab-org/gitlab-runner/-/issues/1042&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;network per&#xA;build&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&#xA;functionality, you may need to tweak this.)&lt;/p&gt;&#xA;&lt;p&gt;As we&amp;rsquo;re exposing the metadataproxy on port 8000, you&amp;rsquo;ll want to make sure&#xA;that port is firewalled off from the outside; either via &lt;code&gt;iptables&lt;/code&gt; or a&#xA;security group.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# this makes an excellent addition to /etc/rc.local&#xA;LOCAL_IPV4=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)&#xA;&#xA;/sbin/iptables \&#xA;  --append PREROUTING \&#xA;  --destination 169.254.169.254 \&#xA;  --protocol tcp \&#xA;  --dport 80 \&#xA;  --in-interface docker&amp;#43; \&#xA;  --jump DNAT \&#xA;  --table nat \&#xA;  --to-destination $LOCAL_IPV4:8000 \&#xA;  --wait&#xA;&#xA;/sbin/iptables \&#xA;  --wait \&#xA;  --insert INPUT 1 \&#xA;  --protocol tcp \&#xA;  --dport 80 \&#xA;  \! \&#xA;  --in-interface docker0 \&#xA;  --jump DROP&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;iam-role-requirements&#34;&gt;&lt;span&gt;IAM role requirements&lt;/span&gt;&#xA;  &lt;a href=&#34;#iam-role-requirements&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;h3 class=&#34;heading-element&#34; id=&#34;ec2-instance-profile&#34;&gt;&lt;span&gt;EC2 Instance Profile&lt;/span&gt;&#xA;  &lt;a href=&#34;#ec2-instance-profile&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;The role belonging to the instance profile associated with the instance our&#xA;agent lives on should be able to assume the roles we want to allow CI jobs to&#xA;assume.  Specifically, the trust policy must permit &lt;code&gt;iam:GetRole&lt;/code&gt; and&#xA;&lt;code&gt;sts:AssumeRole&lt;/code&gt; on these roles.&lt;/p&gt;&#xA;&lt;p&gt;If you&amp;rsquo;re using S3 for &lt;a href=&#34;https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;shared runner caches&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;, you may wish to&#xA;permit this access through the instance profile role as well.  (Implemented&#xA;properly, the proxy will not permit direct CI jobs to use this role.)&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;container--job-iam-roles-for-assumption&#34;&gt;&lt;span&gt;Container / Job IAM roles for assumption&lt;/span&gt;&#xA;  &lt;a href=&#34;#container--job-iam-roles-for-assumption&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;As before, only containers with &lt;code&gt;IAM_ROLE&lt;/code&gt; set at the container level will&#xA;have tokens returned to them by the metadata proxy&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a href=&#34;#fn:5&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, and then only if the&#xA;proxy can successfully assume and convince STS to issue tokens for them.  For&#xA;this to happen, the container/job role&amp;rsquo;s trust policy must alllows the role of&#xA;the instance profile associated with the EC2 instance to assume them.&#xA;Specifically, the trust policy must permit &lt;code&gt;iam:GetRole&lt;/code&gt; and &lt;code&gt;sts:AssumeRole&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading-element&#34; id=&#34;profit&#34;&gt;&lt;span&gt;Profit!&lt;/span&gt;&#xA;  &lt;a href=&#34;#profit&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h1&gt;&lt;p&gt;Alright!  You should now have a good idea as to how create and run CI jobs&#xA;that:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;CANNOT request tokens directly from the EC2 metadata service&lt;/li&gt;&#xA;&lt;li&gt;CANNOT implicitly assume the EC2 instance profile&amp;rsquo;s role&lt;/li&gt;&#xA;&lt;li&gt;CANNOT leak static or long-lived credentials&lt;/li&gt;&#xA;&lt;li&gt;CAN transparently assume certain specific roles&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Enjoy :)&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;The nomenclature gets a bit tricky here.&lt;/p&gt;&#xA;&lt;dl&gt;&#xA;&lt;dt&gt;&lt;code&gt;gitlab-runner&lt;/code&gt;&lt;/dt&gt;&#xA;&lt;dd&gt;The agent responsible for running one or more runner configurations.&lt;/dd&gt;&#xA;&lt;dt&gt;A &amp;ldquo;runner&amp;rdquo;&lt;/dt&gt;&#xA;&lt;dd&gt;A single runner configuration being handled by the &lt;code&gt;gitlab-runner&lt;/code&gt; agent.&lt;/dd&gt;&#xA;&lt;dd&gt;An entity that can run CI jobs, from the perspective of the CI server (e.g.&#xA;gitlab.com proper).&lt;/dd&gt;&#xA;&lt;/dl&gt;&#xA;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;Lyft also has an excellent tool at&#xA;&lt;a href=&#34;https://github.com/lyft/metadataproxy&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://github.com/lyft/metadataproxy&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.  I&amp;rsquo;ve used it with success, but&#xA;&lt;code&gt;go-metadataproxy&lt;/code&gt; provides at least rudimentary metrics for scraping.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;Not that anyone would ever create a trust policy like that, or that it&#xA;would be one of the defaults offered by the AWS web console.  Nope.&#xA;That would never happen.&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:4&#34;&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Alice_and_Bob&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://en.wikipedia.org/wiki/Alice_and_Bob&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:4&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:5&#34;&gt;&#xA;&lt;p&gt;Unless, of course, the metadata proxy is configured with a default role&#xA;&amp;ndash; but we&amp;rsquo;re not going to do that here.&amp;#160;&lt;a href=&#34;#fnref:5&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;</description>
    </item>
    <item>
      <title>Additional GitLab Metrics using `pg_exporter`</title>
      <link>https://weyl.io/2020/08/additional-gitlab-metrics-with-pg-exporter/</link>
      <pubDate>Thu, 27 Aug 2020 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2020/08/additional-gitlab-metrics-with-pg-exporter/</guid>
      <category domain="https://weyl.io/categories/gitlab/">GitLab</category>
      <description>&lt;img src=&#34;https://weyl.io/2020/08/additional-gitlab-metrics-with-pg-exporter/bottles.jpg&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;GitLab makes a great deal of information available through Prometheus metrics.&#xA;But not everything.&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition info&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-circle-info&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;sql_exporter&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;p&gt;Since writing this article, it has come to my attention that there are two&#xA;more generic &amp;ldquo;&lt;code&gt;sql_exporter&lt;/code&gt;&amp;rdquo; options.  This is less important with GitLab, as&#xA;you&amp;rsquo;re basically &lt;em&gt;going&lt;/em&gt; to be running Pg, but these are the generic SQL&#xA;exporter we&amp;rsquo;re turning &lt;code&gt;postgres_exporter&lt;/code&gt; into, below.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/free/sql_exporter&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://github.com/free/sql_exporter&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/justwatchcom/sql_exporter&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;https://github.com/justwatchcom/sql_exporter&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;The other day I was looking for how many CI jobs and pipelines had been&#xA;created, total.  I figured that would be somewhere in the collection of&#xA;existing metrics, but the closest I could find was a metric that gave the&#xA;totals relative to the last time the exporter was restarted.&lt;/p&gt;&#xA;&lt;p&gt;I use a couple GitLab-specific exporters in this environment already, and&#xA;thought about creating another one to handle this.  As it turns out, this&#xA;information isn&amp;rsquo;t exposed through the API, either.  It looked like the only&#xA;way to get this information was to query the database directly.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;--extendquery-path&#34;&gt;&lt;span&gt;&lt;code&gt;--extend.query-path&lt;/code&gt;&lt;/span&gt;&#xA;  &lt;a href=&#34;#--extendquery-path&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;While poking around, I noticed that &lt;a href=&#34;https://github.com/wrouesnel/postgres_exporter&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&lt;code&gt;postgres_exporter&lt;/code&gt;&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&#xA;has an interesting flag, &lt;code&gt;--extend.query-path&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition quote open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-quote-right&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&amp;ndash;extend.query-path&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;Path to a YAML file containing custom queries to run. Check out queries.yaml for examples of the format.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;I did as suggested and checked out the&#xA;&lt;a href=&#34;https://github.com/wrouesnel/postgres_exporter/blob/master/queries.yaml&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;queries.yml&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&#xA;Turns out it&amp;rsquo;s surprisingly easy to create new metrics out of database&#xA;queries, e.g.:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;ci_builds:&#xA;  query: &amp;#34;SELECT MAX(id) as total from ci_builds&amp;#34;&#xA;  metrics:&#xA;    - total:&#xA;        usage: &amp;#34;COUNTER&amp;#34;&#xA;        description: &amp;#34;Total builds created&amp;#34;&#xA;&#xA;ci_pipelines:&#xA;  query: &amp;#34;SELECT MAX(id) as total from ci_pipelines&amp;#34;&#xA;  metrics:&#xA;    - total:&#xA;        usage: &amp;#34;COUNTER&amp;#34;&#xA;        description: &amp;#34;Total pipelines created&amp;#34;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The above causes two additional metrics to be generated by the exporter:&#xA;&lt;code&gt;ci_builds_total&lt;/code&gt; and &lt;code&gt;ci_pipelines_total&lt;/code&gt;.  Neat.  To get the information I&#xA;want, all I need to do is ask &lt;code&gt;postgres_exporter&lt;/code&gt; nicely for it.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;configuring-the-exporter&#34;&gt;&lt;span&gt;Configuring the exporter&lt;/span&gt;&#xA;  &lt;a href=&#34;#configuring-the-exporter&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;The GitLab Omnibus package sets up a number of exporters, including&#xA;&lt;code&gt;postgres_exporter&lt;/code&gt; with &lt;code&gt;--extend.query-path&lt;/code&gt; already set.  However, messing&#xA;around with a configuration file the omnibus package is responsible for did&#xA;not sound like fun, and neither did I want to cause the same Pg metrics to be&#xA;exported twice.  Examining the exporter&amp;rsquo;s flags again, I see two that may&#xA;help.&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition quote open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-quote-right&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;postgresql_exporter documentation&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;disable-default-metrics&lt;/code&gt; Use only metrics supplied from &lt;code&gt;queries.yaml&lt;/code&gt; via&#xA;&lt;code&gt;--extend.query-path&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;disable-settings-metrics&lt;/code&gt; Use the flag if you don&amp;rsquo;t want to scrape&#xA;&lt;code&gt;pg_settings.&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;Looking at those two flags, it appears that I should be able to disable the&#xA;&amp;ldquo;standard&amp;rdquo; metrics and only run the ones I provide in the query file.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;running-the-exporter&#34;&gt;&lt;span&gt;Running the exporter&lt;/span&gt;&#xA;  &lt;a href=&#34;#running-the-exporter&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;To me, it seems easiest to run our custom &lt;code&gt;postgresql_exporter&lt;/code&gt; in parallel&#xA;with the GitLab supplied one.  Running it in a container also allows us to&#xA;ensure it keeps running (&lt;code&gt;--restart&lt;/code&gt;) and runs as the correct user/group for&#xA;access.&lt;/p&gt;&#xA;&lt;div class=&#34;details admonition warning open&#34;&gt;&#xA;  &lt;div class=&#34;details-summary admonition-title&#34;&gt;&lt;i class=&#34;icon fa-fw fa-solid fa-exclamation-triangle&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;You get to keep both parts&lt;i class=&#34;details-icon fa-solid fa-angle-right fa-fw&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/div&gt;&#xA;  &lt;div class=&#34;details-content&#34;&gt;&#xA;    &lt;div class=&#34;admonition-content&#34;&gt;This involves configuring a tool for direct access to your GitLab instance&amp;rsquo;s&#xA;database.  While the &lt;code&gt;postgres_exporter&lt;/code&gt; is a widely-used and reliable tool,&#xA;&lt;em&gt;if you break your server you get to keep both parts&lt;/em&gt;.&lt;/div&gt;&#xA;  &lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;A small scriptie to start the exporter, disable standard metrics, and run ours&#xA;is below.  Note that it also ensures it is run as the correct user/group for&#xA;database access, and the Pg socket + configuration is bind-mounted inside the&#xA;container for access.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;PG_USER=&amp;#34;${PG_USER:-gitlab-psql}&amp;#34;&#xA;PG_UID=&amp;#34;$(id -u $PG_USER)&amp;#34;&#xA;PG_GID=&amp;#34;$(id -g $PG_USER)&amp;#34;&#xA;&#xA;docker run -d \&#xA;    --name gitlab-custom-metrics \&#xA;    --restart unless-stopped \&#xA;    --user $PG_UID:$PG_GID \&#xA;    --publish 19187:9187 \&#xA;    -v /var/opt/gitlab/postgresql:/var/opt/gitlab/postgresql \&#xA;        -v `pwd`/queries.yml:/queries.yml:ro \&#xA;    -e DATA_SOURCE_NAME=&amp;#34;user=$PG_USER host=/var/opt/gitlab/postgresql database=gitlabhq_production&amp;#34; \&#xA;    wrouesnel/postgres_exporter \&#xA;        --disable-default-metrics \&#xA;        --disable-settings-metrics \&#xA;        --extend.query-path /queries.yml&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With that, the metrics exporter is exposed and ready to be scraped at&#xA;&lt;code&gt;localhost:19187/metrics&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;# HELP ci_builds_total Total builds created&#xA;# TYPE ci_builds_total counter&#xA;ci_builds_total{server=&amp;#34;/var/opt/gitlab/postgresql:5432&amp;#34;} 437695&#xA;# HELP ci_pipelines_total Total pipelines created&#xA;# TYPE ci_pipelines_total counter&#xA;ci_pipelines_total{server=&amp;#34;/var/opt/gitlab/postgresql:5432&amp;#34;} 67665&lt;/code&gt;&lt;/pre&gt;&lt;h2 class=&#34;heading-element&#34; id=&#34;conclusion&#34;&gt;&lt;span&gt;Conclusion&lt;/span&gt;&#xA;  &lt;a href=&#34;#conclusion&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;With this in place, we can collect and display or alert on these custom&#xA;metrics.  And, of course, everyone loves a good dashboard graph:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#39;https://weyl.io/2020/08/additional-gitlab-metrics-with-pg-exporter/grafana-pipeline-stats.png&#39; alt=&#34;Grafana charts using the custom metrics&#34; height=&#34;477&#34; width=&#34;562&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;This might seem like a lot for two small metrics, but compared to writing a&#xA;custom exporter it&amp;rsquo;s nothing.  If you&amp;rsquo;re like me, you&amp;rsquo;ll also discover your&#xA;&lt;code&gt;queries.yml&lt;/code&gt; will quickly grow with additional metrics definitions.&lt;/p&gt;</description>
    </item>
    <item>
      <title>KMS key context, IAM conditions, and s3</title>
      <link>https://weyl.io/2020/08/kms-key-context-and-s3/</link>
      <pubDate>Sun, 23 Aug 2020 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2020/08/kms-key-context-and-s3/</guid>
      <category domain="https://weyl.io/categories/aws/">AWS</category>
      <description>&lt;img src=&#34;https://weyl.io/2020/08/kms-key-context-and-s3/keys.jpg&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;At $work, I&amp;rsquo;ve been using KMS to encrypt s3 bucket contents for some time now.&#xA;It works rather well, but one thing that had been bugging me is that our IAM&#xA;policies granted both read permissions on bucket objects and encrypt/decrypt&#xA;on the relevant KMS key.  That is, principals with the policies attached can&#xA;use the key to encrypt/decrypt anything they otherwise have permission to&#xA;access, not just objects in the bucket.  It didn&amp;rsquo;t appear that there was a&#xA;reasonable way to tighten this until I ran across references to &lt;a href=&#34;https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-encryption-context&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;the IAM&#xA;&lt;code&gt;kms:EncryptionContext:&lt;/code&gt; condition&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Using &lt;code&gt;kms:EncryptionContext:&lt;/code&gt; it is possible to conditionally restrict a&#xA;policy based on the ARN of the resource being acted upon.  That is to say, one&#xA;can use this condition to only allow a KMS key to be used to decrypt objects&#xA;in a certain s3 bucket.&lt;/p&gt;&#xA;&lt;p&gt;It took me a bit to figure this out as the docs didn&amp;rsquo;t quite spell out how to&#xA;use an ARN as an encryption context (and, you know, encryption), so here&amp;rsquo;s a&#xA;policy that shows it in action.  The policy allows actions a certain KMS key&#xA;to be used only in the context of the given bucket:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;{&#xA;  &amp;#34;Version&amp;#34;: &amp;#34;2012-10-17&amp;#34;,&#xA;  &amp;#34;Statement&amp;#34;: [&#xA;    {&#xA;      &amp;#34;Sid&amp;#34;: &amp;#34;kmsKeyAccess&amp;#34;,&#xA;      &amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;,&#xA;      &amp;#34;Action&amp;#34;: [&#xA;        &amp;#34;kms:ReEncrypt&amp;#34;,&#xA;        &amp;#34;kms:GenerateDataKey*&amp;#34;,&#xA;        &amp;#34;kms:Encrypt&amp;#34;,&#xA;        &amp;#34;kms:Decrypt&amp;#34;&#xA;      ],&#xA;      &amp;#34;Resource&amp;#34;: &amp;#34;arn:aws:kms:us-west-2:12345678901234:key/1234-abcd-567890-12345&amp;#34;,&#xA;      &amp;#34;Condition&amp;#34;: {&#xA;        &amp;#34;StringLike&amp;#34;: {&#xA;          &amp;#34;kms:EncryptionContext:aws:s3:arn&amp;#34;: &amp;#34;arn:aws:s3:::davros-world-domination/*&amp;#34;&#xA;        }&#xA;      }&#xA;    }&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;These actions are only granted on objects in the specific s3 bucket and not&#xA;denied explicitly to anything else.  This is important, as we don&amp;rsquo;t want our&#xA;efforts here to block a legitimate grant somewhere else.&lt;/p&gt;&#xA;&lt;p&gt;This can be extended to apply to multiple buckets by changing the condition&#xA;test to &lt;code&gt;ForAnyValue:StringLike&lt;/code&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Conditional git Configuration</title>
      <link>https://weyl.io/2019/03/conditional-git-includes/</link>
      <pubDate>Thu, 21 Mar 2019 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2019/03/conditional-git-includes/</guid>
      <category domain="https://weyl.io/categories/dev-tooling/">Dev Tooling</category>
      <description>&lt;img src=&#34;https://weyl.io/2019/03/conditional-git-includes/pikwizard-traffic-lights-with-red-light-on.jpg&#34; alt=&#34;featured image&#34; referrerpolicy=&#34;no-referrer&#34;&gt;&lt;p&gt;git has always(?) allowed for additional configuration files to be&#xA;unconditionally included:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[include]&#xA;    path = path/to/gitconfig&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Each individual git repo has always had the ability to maintain its own&#xA;configuration at &lt;code&gt;.git/config&lt;/code&gt;.  However, sometimes on our systems we also&#xA;have certain &lt;em&gt;locations&lt;/em&gt; where we store multiple git projects, which may need&#xA;different configuration from the global, but still common across that&#xA;location.&lt;/p&gt;&#xA;&lt;p&gt;Since &amp;hellip; well, for the last year or two at least, git has allowed for the&#xA;conditional inclusion of configuration files.&lt;/p&gt;&#xA;&lt;p&gt;For example, I contribute to F/OSS projects using one email address, which&#xA;lives in my global git config .  However, for work projects, I want to use my&#xA;work email everywhere —  and accidentally pushing w/my personal email address&#xA;is just embarrassing.  All of my work projects live under a certain directory,&#xA;so I can tell git that if a given repository&amp;rsquo;s gitdir lives under &lt;code&gt;~/work&lt;/code&gt;, it&#xA;should also load an additional configuration file:&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;[includeIf &amp;#34;gitdir:~/work/&amp;#34;]&#xA;    path = ~/work/gitconfig&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&amp;hellip;and in there, I can set&lt;/p&gt;&#xA;&lt;pre&gt;&lt;code&gt;; this is ~/work/gitconfig&#xA;[user]&#xA;    email = cweyl@work.com&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In this way, I do not need to remember to change the email address of any&#xA;repos I clone under &lt;code&gt;~/work&lt;/code&gt; to my work address.  This is especially useful as&#xA;I not infrequently find myself forking and submitting bugfix PR/MR&amp;rsquo;s to&#xA;upstream, and if I do that for &lt;code&gt;$work&lt;/code&gt; then I want to be using my work email&#xA;address.&lt;/p&gt;&#xA;&lt;p&gt;See also the &lt;a href=&#34;https://git-scm.com/docs/git-config#_includes&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&amp;ldquo;Includes&amp;rdquo;&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;&#xA;and &lt;a href=&#34;https://git-scm.com/docs/git-config#_conditional_includes&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;&amp;ldquo;Conditional Includes&amp;rdquo; sections of the &lt;code&gt;git-config&lt;/code&gt; manpage&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>First Post</title>
      <link>https://weyl.io/2018/02/first-post/</link>
      <pubDate>Wed, 28 Feb 2018 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2018/02/first-post/</guid>
      <description>&lt;p&gt;&amp;hellip;um, kinda.  I&amp;rsquo;m switching over from wordpress to&#xA;&lt;a href=&#34;http://preaction.me/statocles/&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;Statocles&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;, and porting my older posts over.&lt;/p&gt;&#xA;&lt;p&gt;Still, who can resist &amp;ldquo;first post!&amp;rdquo; ;)&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Fast Project Finding With fzf</title>
      <link>https://weyl.io/2018/02/fast-project-finding-with-fzf/</link>
      <pubDate>Sat, 24 Feb 2018 00:00:00 +0000</pubDate><author>chris@weyl.io (Chris Weyl)</author>
      <guid>https://weyl.io/2018/02/fast-project-finding-with-fzf/</guid>
      <category domain="https://weyl.io/categories/dev-tooling/">Dev Tooling</category>
      <description>&lt;p&gt;&lt;a href=&#34;https://github.com/junegunn/fzf&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;fzf&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; is a fantastic utility, written by an &lt;a href=&#34;https://github.com/junegunn&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;author&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt; with a history of writing&#xA;useful things.  He&amp;rsquo;s also a vim user, and in addition to his other vim plugins&#xA;he has created an &amp;ldquo;enhancement&amp;rdquo; plugin called &lt;a href=&#34;https://github.com/junegunn/fzf.vim&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;fzf.vim&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;One of the neat things &lt;code&gt;fzf.vim&lt;/code&gt; does is make it easy to create new commands&#xA;for fuzzy searches.  If you&amp;rsquo;re like me, you probably have some absurd number of project&#xA;repositories you keep around and jump to, as necessary.  Not everything is in&#xA;the same directory (e.g. &lt;code&gt;~/work/&lt;/code&gt;), naturally, and with a laptop, desktop,&#xA;and a couple other machines the less-frequently used repos may be where one&#xA;least expects them to be — or not present at all.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s not hugely annoying, just a sort of mild pain to have to spend several&#xA;extra seconds doing a fuzzy search manually, rather than having &lt;code&gt;fzf&lt;/code&gt; do it.&#xA;But we do have &lt;code&gt;fzf&lt;/code&gt;, and it&amp;rsquo;s not difficult at all to build out a new search,&#xA;so there&amp;rsquo;s really no reason to keep on inflicting that pain.&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;create-a-projects-command&#34;&gt;&lt;span&gt;Create a &lt;code&gt;:Projects&lt;/code&gt; command&lt;/span&gt;&#xA;  &lt;a href=&#34;#create-a-projects-command&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s &lt;a href=&#34;https://github.com/rsrchboy/vimfiles/blob/b243f251b3c8965e7d5dc4e4655819be27bd7a90/vimrc#L339-L344&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;create a new command in my vimrc&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;,&#xA;&lt;code&gt;:Projects&lt;/code&gt;, that invokes &lt;code&gt;fzf&lt;/code&gt; to search through all the different work&#xA;directories I have.&lt;/p&gt;&#xA;&lt;!-- &lt;script src=&#34;https://gist.github.com/rsrchboy/42fa288e65741500819c6341bde5caa5.js?file=projects.vim&#34;&gt;&lt;/script&gt; --&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;command&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;!&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;nargs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Projects&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    \ &lt;span class=&#34;nx&#34;&gt;call&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;fzf&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fzf&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;wrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;projects&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;source&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;find ~/work ~/.vim/plugged -name .git -maxdepth 3 -printf &amp;#39;&amp;#39;%h\n&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;sink&amp;#39;&lt;/span&gt;: &lt;span class=&#34;k&#34;&gt;function&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;rsrchboy#fzf#FindOrOpenTab&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;options&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;-m --prompt &amp;#34;Projects&amp;gt; &amp;#34;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    \}&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;bang&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;What does this do?&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Defines a new vim command, &lt;code&gt;:Projects&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;No surprises here.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Invokes &lt;code&gt;fzf#run()&lt;/code&gt; to run a &lt;code&gt;fzf&lt;/code&gt; search&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;fzf#run()&lt;/code&gt; handles the actual execution and presentation of &lt;code&gt;fzf&lt;/code&gt;, as&#xA;well has dispatching the results back to the &lt;code&gt;sink&lt;/code&gt;.  &lt;code&gt;fzf#wrap()&lt;/code&gt; is&#xA;neat.  It allows a command to take advantage of &lt;code&gt;fzf.vim&lt;/code&gt;&amp;rsquo;s option&#xA;handling &amp;ndash; or not, by simply omitting it.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Uses &lt;code&gt;find&lt;/code&gt; to look for repositories&lt;/p&gt;&#xA;&lt;p&gt;We know roughly where to look(&lt;code&gt;~/work/&lt;/code&gt;, &lt;code&gt;~/.vim/plugged&lt;/code&gt;) and how deep to&#xA;look.  Just about everything I do is backed by git, so we can look for&#xA;repositories and return the parent of the found &lt;code&gt;.git&lt;/code&gt; back to &lt;code&gt;fzf&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Note that the &lt;code&gt;find&lt;/code&gt; invocation deliberately omits a &lt;code&gt;-type d&lt;/code&gt; argument.&#xA;I do use &lt;a href=&#34;https://git-scm.com/docs/git-worktree&#34; target=&#34;_blank&#34; rel=&#34;external nofollow noopener noreferrer&#34;&gt;git worktrees&lt;i class=&#34;fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary&#34; aria-hidden=&#34;true&#34;&gt;&lt;/i&gt;&lt;/a&gt;, meaning &lt;code&gt;.git&lt;/code&gt; may well be a file (a &amp;ldquo;gitlink&amp;rdquo;).&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Calls out to &lt;code&gt;rsrchboy#fzf#FindOrOpenTab()&lt;/code&gt; with the project selected&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;sink&lt;/code&gt; option tells &lt;code&gt;fzf#run()&lt;/code&gt; what to do with the results.  In our&#xA;case we have provided &lt;code&gt;fzf#run()&lt;/code&gt; with a callback function, but you can&#xA;also use built-ins as sinks.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading-element&#34; id=&#34;the-callback-sink-function&#34;&gt;&lt;span&gt;The callback &amp;ldquo;sink&amp;rdquo; function&lt;/span&gt;&#xA;  &lt;a href=&#34;#the-callback-sink-function&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h2&gt;&lt;!-- &lt;script src=&#34;https://gist.github.com/rsrchboy/42fa288e65741500819c6341bde5caa5.js?file=autoload-rb-fzf.vim&#34;&gt;&lt;/script&gt; --&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;20&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;21&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;22&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-vim&#34; data-lang=&#34;vim&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;fun&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;!&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;rsrchboy&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;fzf&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;FindOrOpenTab&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;work_dir&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;abort&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;    &amp;#34; loop over our tabs, looking for one with a t:git_workdir matching our&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;    &amp;#34; a:workdir; if found, change tab; if not fire up fzf again to find a file&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;    &amp;#34; to open in the new tab&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;l&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;tab&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;gettabinfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;l&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;tab&lt;/span&gt;.&lt;span class=&#34;nx&#34;&gt;variables&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;git_workdir&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;==&lt;/span&gt;# &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;work_dir&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nx&#34;&gt;exe&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;tabn &amp;#39;&lt;/span&gt; . &lt;span class=&#34;nx&#34;&gt;l&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;tab&lt;/span&gt;.&lt;span class=&#34;nx&#34;&gt;tabnr&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;nx&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;k&#34;&gt;endif&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;endfor&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;call&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;fzf&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fzf&lt;/span&gt;#&lt;span class=&#34;nx&#34;&gt;wrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;other-repo-git-ls&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; {&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;source&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;git ls-files&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;dir&amp;#39;&lt;/span&gt;: &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;work_dir&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;options&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;--prompt &amp;#34;GitFiles in &amp;#39;&lt;/span&gt; . &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;:&lt;span class=&#34;nx&#34;&gt;work_dir&lt;/span&gt; . &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;gt; &amp;#34;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        \   &lt;span class=&#34;s1&#34;&gt;&amp;#39;sink&amp;#39;&lt;/span&gt;: &lt;span class=&#34;s1&#34;&gt;&amp;#39;tabe &amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        \}&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;return&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;endfun&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;In general, I use one tab per project (repository) in vim.  For me, this is a&#xA;nice balance of utility and sanity.  It also allows me to do things like set&#xA;&lt;code&gt;t:git_dir&lt;/code&gt; and &lt;code&gt;t:git_workdir&lt;/code&gt; to the git and workdir, respectively, of the&#xA;repository associated with the tab.&lt;/p&gt;&#xA;&lt;p&gt;Our callback function first attempts to find an open tab with the workdir&#xA;requested; if found, it just switches to it and returns.  (It should probably&#xA;admonish me to read the tab line before invoking &lt;code&gt;:Projects&lt;/code&gt;.)&lt;/p&gt;&#xA;&lt;p&gt;If not found. the callback function invokes &lt;code&gt;fzf#run()&lt;/code&gt; again.  This time we&#xA;use &lt;code&gt;git ls-files&lt;/code&gt; to generate the source list for &lt;code&gt;fzf&lt;/code&gt;, allowing us to pick&#xA;a file to be opened by the given &lt;code&gt;sink&lt;/code&gt;: &lt;code&gt;tabe&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading-element&#34; id=&#34;hey-that-wasnt-too-hard&#34;&gt;&lt;span&gt;Hey, that wasn&amp;rsquo;t too hard!&lt;/span&gt;&#xA;  &lt;a href=&#34;#hey-that-wasnt-too-hard&#34; class=&#34;heading-mark&#34;&gt;&#xA;    &lt;svg class=&#34;octicon octicon-link&#34; viewBox=&#34;0 0 16 16&#34; version=&#34;1.1&#34; width=&#34;16&#34; height=&#34;16&#34; aria-hidden=&#34;true&#34;&gt;&lt;path d=&#34;m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z&#34;&gt;&lt;/path&gt;&lt;/svg&gt;&#xA;  &lt;/a&gt;&#xA;&lt;/h3&gt;&lt;p&gt;Easier than writing this post, I&amp;rsquo;d say ;)&lt;/p&gt;&#xA;&lt;p&gt;Happy hacking!&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>
