<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.lidalia.org.uk/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.lidalia.org.uk/" rel="alternate" type="text/html" /><updated>2026-09-08T15:13:57+00:00</updated><id>https://blog.lidalia.org.uk/feed.xml</id><title type="html">Software Development Thoughts</title><subtitle>Thoughts on Software Development</subtitle><author><name>Robert Elliot</name><email>rob@lidalia.org.uk</email></author><entry><title type="html">Standard HTTP Operability Endpoints</title><link href="https://blog.lidalia.org.uk/2026/09/standard-http-operability-endpoints.html" rel="alternate" type="text/html" title="Standard HTTP Operability Endpoints" /><published>2026-09-04T10:42:14+00:00</published><updated>2026-09-04T10:42:14+00:00</updated><id>https://blog.lidalia.org.uk/2026/09/standard-http-operability-endpoints</id><content type="html" xml:base="https://blog.lidalia.org.uk/2026/09/standard-http-operability-endpoints.html"><![CDATA[<p>A standard set of HTTP endpoints a service should expose for the infrastructure that runs it. Recommendations first,
reasoning after.</p>

<h2 id="recommendations">Recommendations</h2>

<p>One port, the one that serves traffic. Everything operability lives under <code class="language-plaintext highlighter-rouge">/-/</code>, which is itself an index of the
rest. Restrict access to <code class="language-plaintext highlighter-rouge">/-/</code> by path at the ingress, except <code class="language-plaintext highlighter-rouge">/-/up</code>, which is public.</p>

<table>
  <thead>
    <tr>
      <th>Endpoint</th>
      <th>For</th>
      <th>Response</th>
      <th>Content-Type</th>
      <th>Format</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/</code></td>
      <td>Discovery</td>
      <td>200. <code class="language-plaintext highlighter-rouge">_links</code> to the six endpoints below, keyed <code class="language-plaintext highlighter-rouge">live</code>, <code class="language-plaintext highlighter-rouge">ready</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">up</code>, <code class="language-plaintext highlighter-rouge">info</code>, <code class="language-plaintext highlighter-rouge">metrics</code>.</td>
      <td><code class="language-plaintext highlighter-rouge">application/hal+json</code></td>
      <td><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-11">HAL</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/live</code></td>
      <td>Restart checks</td>
      <td>200 <code class="language-plaintext highlighter-rouge">pass</code>, or 503 <code class="language-plaintext highlighter-rouge">fail</code> if the process is broken. <code class="language-plaintext highlighter-rouge">checks</code> lists the liveness checks.</td>
      <td><code class="language-plaintext highlighter-rouge">application/health+json</code></td>
      <td><a href="https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06">IETF health check draft</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/ready</code></td>
      <td>Routing checks</td>
      <td>503 <code class="language-plaintext highlighter-rouge">fail</code> while starting, draining or not the leader, else 200 <code class="language-plaintext highlighter-rouge">pass</code>. <code class="language-plaintext highlighter-rouge">checks</code> lists the lifecycle checks.</td>
      <td><code class="language-plaintext highlighter-rouge">application/health+json</code></td>
      <td><a href="https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06">IETF health check draft</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/status</code></td>
      <td>Humans and alerting</td>
      <td>200 <code class="language-plaintext highlighter-rouge">pass</code> or <code class="language-plaintext highlighter-rouge">warn</code>, 503 <code class="language-plaintext highlighter-rouge">fail</code>. <code class="language-plaintext highlighter-rouge">checks</code> lists everything: liveness, lifecycle, pressure, dependencies.</td>
      <td><code class="language-plaintext highlighter-rouge">application/health+json</code></td>
      <td><a href="https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06">IETF health check draft</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/up</code></td>
      <td>Uptime monitors</td>
      <td><code class="language-plaintext highlighter-rouge">/-/status</code> with <code class="language-plaintext highlighter-rouge">checks</code> removed. Public.</td>
      <td><code class="language-plaintext highlighter-rouge">application/health+json</code></td>
      <td><a href="https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06">IETF health check draft</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/info</code></td>
      <td>Humans and tooling</td>
      <td>The process’s OpenTelemetry Resource, dotted keys nested.</td>
      <td><code class="language-plaintext highlighter-rouge">application/json</code></td>
      <td><a href="https://opentelemetry.io/docs/specs/semconv/resource/">OTel resource semconv</a></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/-/metrics</code></td>
      <td>Scrapers</td>
      <td>Current metric values.</td>
      <td><code class="language-plaintext highlighter-rouge">application/openmetrics-text; version=1.0.0; charset=utf-8</code></td>
      <td><a href="https://github.com/prometheus/OpenMetrics/blob/main/specification/OpenMetrics.md">OpenMetrics</a></td>
    </tr>
  </tbody>
</table>

<p>Wire the platform’s checks up like this:</p>

<table>
  <thead>
    <tr>
      <th>Check</th>
      <th>Endpoint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Docker <code class="language-plaintext highlighter-rouge">HEALTHCHECK</code></td>
      <td><code class="language-plaintext highlighter-rouge">/-/live</code></td>
    </tr>
    <tr>
      <td>ECS container health check</td>
      <td><code class="language-plaintext highlighter-rouge">/-/live</code></td>
    </tr>
    <tr>
      <td>ALB target group health check</td>
      <td><code class="language-plaintext highlighter-rouge">/-/live</code> on ECS, <code class="language-plaintext highlighter-rouge">/-/ready</code> elsewhere</td>
    </tr>
    <tr>
      <td>K8S liveness probe</td>
      <td><code class="language-plaintext highlighter-rouge">/-/live</code></td>
    </tr>
    <tr>
      <td>K8S readiness probe</td>
      <td><code class="language-plaintext highlighter-rouge">/-/ready</code></td>
    </tr>
    <tr>
      <td>K8S startup probe</td>
      <td><code class="language-plaintext highlighter-rouge">/-/ready</code></td>
    </tr>
    <tr>
      <td>Third-party uptime monitor</td>
      <td><code class="language-plaintext highlighter-rouge">/-/up</code></td>
    </tr>
  </tbody>
</table>

<p>What fails what:</p>

<table>
  <thead>
    <tr>
      <th>State</th>
      <th><code class="language-plaintext highlighter-rouge">/-/live</code></th>
      <th><code class="language-plaintext highlighter-rouge">/-/ready</code></th>
      <th><code class="language-plaintext highlighter-rouge">/-/status</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Broken</strong>: deadlock, leaked memory, wedged event loop</td>
      <td>fail</td>
      <td>fail</td>
      <td>fail</td>
    </tr>
    <tr>
      <td><strong>Lifecycle</strong>: starting up, draining on shutdown, not the leader</td>
      <td>pass</td>
      <td>fail</td>
      <td>fail</td>
    </tr>
    <tr>
      <td><strong>Under pressure</strong>: high CPU, GC storms, saturated pools</td>
      <td>pass</td>
      <td>pass</td>
      <td>warn</td>
    </tr>
    <tr>
      <td><strong>Dependency down</strong>, needed by all traffic: the DB</td>
      <td>pass</td>
      <td>pass</td>
      <td>fail</td>
    </tr>
    <tr>
      <td><strong>Dependency down</strong>, needed by some routes: one upstream of many</td>
      <td>pass</td>
      <td>pass</td>
      <td>warn</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">/-/status</code> reports the worst status of any check, so it is the one that tells the truth about the instance. A
dependency that every request needs makes it <code class="language-plaintext highlighter-rouge">fail</code>; one that only some routes need makes it <code class="language-plaintext highlighter-rouge">warn</code>, and the check
lists those routes in <code class="language-plaintext highlighter-rouge">affectedEndpoints</code>. <code class="language-plaintext highlighter-rouge">/-/up</code> is the same fold with the checks stripped, so a third party can
watch it without being told anything but up or down.</p>

<p>Checks run on a timer, not per request. Every endpoint serves the latest snapshot, and each check’s <code class="language-plaintext highlighter-rouge">time</code> says when
it was taken.</p>

<p>Dependency failures are handled in the application, by returning 503 with <code class="language-plaintext highlighter-rouge">Retry-After</code> on the routes that need the
dependency, not by the infrastructure taking the instance out.</p>

<p>503 is the failure code because every caller agrees on it: the kubelet fails on anything outside 200-399, the ALB
matcher defaults to 200, and <code class="language-plaintext highlighter-rouge">curl -f</code> exits non-zero on 400 and above. In practice a broken process more often times
out than answers 503, so probe timeouts matter as much as the response.</p>

<p><code class="language-plaintext highlighter-rouge">/-/status</code> on an instance whose database is unreachable looks like:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fail"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"checks"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"event-loop:lag"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pass"</span><span class="p">,</span><span class="w"> </span><span class="nl">"observedValue"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="nl">"observedUnit"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ms"</span><span class="p">,</span><span class="w"> </span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-09-04T10:42:14Z"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"lifecycle:phase"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pass"</span><span class="p">,</span><span class="w"> </span><span class="nl">"observedValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"serving"</span><span class="p">,</span><span class="w"> </span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-09-04T10:42:14Z"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"jvm:heap"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"warn"</span><span class="p">,</span><span class="w"> </span><span class="nl">"observedValue"</span><span class="p">:</span><span class="w"> </span><span class="mi">91</span><span class="p">,</span><span class="w"> </span><span class="nl">"observedUnit"</span><span class="p">:</span><span class="w"> </span><span class="s2">"percent"</span><span class="p">,</span><span class="w"> </span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-09-04T10:42:14Z"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">],</span><span class="w">
    </span><span class="nl">"postgres:connectivity"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fail"</span><span class="p">,</span><span class="w"> </span><span class="nl">"componentType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"datastore"</span><span class="p">,</span><span class="w"> </span><span class="nl">"output"</span><span class="p">:</span><span class="w"> </span><span class="s2">"connection refused"</span><span class="p">,</span><span class="w"> </span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-09-04T10:42:11Z"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Keys in <code class="language-plaintext highlighter-rouge">checks</code> are <code class="language-plaintext highlighter-rouge">component:measurement</code>. Each maps to an array, since a component may be observed more than
once. The top-level <code class="language-plaintext highlighter-rouge">status</code> is the worst of the checks. <code class="language-plaintext highlighter-rouge">/-/live</code> here would return only <code class="language-plaintext highlighter-rouge">event-loop:lag</code>, and
<code class="language-plaintext highlighter-rouge">/-/ready</code> only <code class="language-plaintext highlighter-rouge">lifecycle:phase</code>, both <code class="language-plaintext highlighter-rouge">pass</code>.</p>

<p><code class="language-plaintext highlighter-rouge">/-/info</code> looks like:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"service"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.14.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders-7c9d8f-x2kq"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"deployment"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"environment"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"prod"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"vcs"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"ref"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"head"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"revision"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a3f9c1e"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"process"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"runtime"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OpenJDK Runtime Environment"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"21.0.4"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="content-negotiation">Content negotiation</h3>

<p>Nice to have. Every endpoint honours <code class="language-plaintext highlighter-rouge">Accept</code>:</p>

<table>
  <thead>
    <tr>
      <th>Requested</th>
      <th>Served</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">application/{type}+json</code></td>
      <td>The formats above. This is the default for <code class="language-plaintext highlighter-rouge">*/*</code> or no <code class="language-plaintext highlighter-rouge">Accept</code> header, so <code class="language-plaintext highlighter-rouge">curl</code> gets JSON.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">application/{type}+yaml</code></td>
      <td>The same document as YAML. The <code class="language-plaintext highlighter-rouge">+yaml</code> suffix is <a href="https://www.rfc-editor.org/rfc/rfc9512">RFC 9512</a>.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">text/html</code></td>
      <td>A page readable without ECMAScript. Browsers ask for this first, so a browser gets a page.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">text/markdown</code>, <code class="language-plaintext highlighter-rouge">text/plain</code></td>
      <td>Markdown, which is also valid plain text.</td>
    </tr>
  </tbody>
</table>

<p>The HTML and Markdown renderings list every <code class="language-plaintext highlighter-rouge">application/*</code> media type the endpoint serves, so a human who finds the
page knows what a machine should ask for. The HTML page also includes the JSON of the same response in a folded
<code class="language-plaintext highlighter-rouge">&lt;details&gt;</code> section, which needs no script. If <code class="language-plaintext highlighter-rouge">Accept</code> matches none of these, serve the default rather than 406.</p>

<p><code class="language-plaintext highlighter-rouge">/-/metrics</code> differs in two rows. <code class="language-plaintext highlighter-rouge">text/plain</code> means the Prometheus text exposition, because that is what scrapers ask
for: Prometheus sends <code class="language-plaintext highlighter-rouge">application/openmetrics-text;version=1.0.0, text/plain;version=0.0.4;q=0.5, */*;q=0.1</code>, and
older scrapers send <code class="language-plaintext highlighter-rouge">text/plain</code> or nothing. For the same reason the default is the text exposition, not JSON. There is
no standard pull-format JSON for metrics; the nearest is the <a href="https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding">OTLP/HTTP JSON encoding</a>, which is a push payload
served off-label, and is what <code class="language-plaintext highlighter-rouge">application/json</code> returns.</p>

<h2 id="why">Why</h2>

<h3 id="the-health-checks-that-will-call-you">The health checks that will call you</h3>

<table>
  <thead>
    <tr>
      <th>Check</th>
      <th>Who runs it</th>
      <th>Where from</th>
      <th>Question it answers</th>
      <th>On failure</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Docker <code class="language-plaintext highlighter-rouge">HEALTHCHECK</code></td>
      <td>Docker daemon</td>
      <td>Inside the container (<code class="language-plaintext highlighter-rouge">exec</code>)</td>
      <td>Is this process working?</td>
      <td>Status flag set to <code class="language-plaintext highlighter-rouge">unhealthy</code>. Nothing else.</td>
    </tr>
    <tr>
      <td>ECS container health check</td>
      <td>ECS agent (same syntax)</td>
      <td>Inside the container (<code class="language-plaintext highlighter-rouge">exec</code>)</td>
      <td>Is this container working?</td>
      <td>Task stopped and replaced.</td>
    </tr>
    <tr>
      <td>ALB target group health check</td>
      <td>Load balancer nodes</td>
      <td>Over the network</td>
      <td>Should I route traffic here?</td>
      <td>Target taken out of rotation. On ECS, task killed.</td>
    </tr>
    <tr>
      <td>K8S liveness probe</td>
      <td>kubelet</td>
      <td>From the node</td>
      <td>Is this process stuck?</td>
      <td>Container restarted.</td>
    </tr>
    <tr>
      <td>K8S readiness probe</td>
      <td>kubelet</td>
      <td>From the node</td>
      <td>Should this pod receive traffic?</td>
      <td>Pod removed from Service endpoints. Not restarted.</td>
    </tr>
    <tr>
      <td>K8S startup probe</td>
      <td>kubelet</td>
      <td>From the node</td>
      <td>Has this container finished booting?</td>
      <td>Liveness suppressed until it passes.</td>
    </tr>
  </tbody>
</table>

<p>Two mechanisms are at work: <em>restart</em> checks (Docker, ECS, liveness) and <em>routing</em> checks (ALB, readiness). Only K8S
keeps them cleanly separate.</p>

<p>Some wrinkles:</p>

<ul>
  <li>Docker’s <code class="language-plaintext highlighter-rouge">HEALTHCHECK</code> on its own restarts nothing. It flips a flag that something else - Swarm, Compose
<code class="language-plaintext highlighter-rouge">depends_on: condition: service_healthy</code>, an <code class="language-plaintext highlighter-rouge">autoheal</code> sidecar, ECS - has to act on.</li>
  <li>ALB checks are routing checks, but ECS treats an ALB-unhealthy target as a failed task and replaces it. So on ECS
a routing check becomes a restart check, which is why the wiring table above sends the ALB to <code class="language-plaintext highlighter-rouge">/-/live</code> on ECS.</li>
  <li>Docker and ECS run the check <em>inside</em> the container, so the image needs <code class="language-plaintext highlighter-rouge">curl</code> or equivalent. ALB and the kubelet
probe over the network, so they also prove the port is bound and reachable.</li>
  <li>K8S’s startup probe has rough equivalents in ECS’s <code class="language-plaintext highlighter-rouge">startPeriod</code> on the container health check and
<code class="language-plaintext highlighter-rouge">healthCheckGracePeriodSeconds</code> on the service for ALB checks.</li>
</ul>

<p>The consequence: a check that fails when a downstream dependency is down is appropriate as a routing check and
harmful as a restart check, since restarting will not fix the dependency. On ECS, where routing checks trigger
restarts, a dependency outage cascades into every task being cycled.</p>

<h3 id="what-can-actually-go-wrong">What can actually go wrong</h3>

<p>Four kinds of unhealthy, and they want different responses.</p>

<p><strong>Broken</strong> - deadlock, leaked memory, wedged event loop. Only a restart will fix it. Fail everything.</p>

<p><strong>Lifecycle</strong> - starting up, draining on shutdown, not the leader. Instance-specific and will resolve itself. Fail
routing, pass restart.</p>

<p><strong>Under pressure</strong> looks like a routing failure - take the hot instance out until it cools. But if load is even, every
instance is hot. Failing readiness across the fleet leaves the Service with no endpoints, and each instance that drops
out sends more load to the survivors. Readiness is a tool for one hot instance; autoscaling is the tool for a hot fleet.
On ECS the question is moot: the ALB check is the only routing check and ECS turns it into a restart check, so pressure
must pass everything.</p>

<p><strong>Dependency down</strong> is worse. It is fleet-wide by nature, so failing readiness everywhere means callers get connection
refused instead of a meaningful 503, new pods never go ready so unrelated rollouts stall, and when the dependency
returns the fleet is not already up and waiting. Instead: return 503 with <code class="language-plaintext highlighter-rouge">Retry-After</code> on the routes that need the
dependency, keep serving the ones that do not, and report dependency status on <code class="language-plaintext highlighter-rouge">/-/status</code> for humans and alerting.
There, a dependency needed by every request is <code class="language-plaintext highlighter-rouge">fail</code> and one needed by some routes is <code class="language-plaintext highlighter-rouge">warn</code>: a SaaS control plane
that proxies to per-customer infrastructure is down when its database is down, not when one customer’s proxy target
is. Nothing an orchestrator acts on should include dependency checks. Keeping <code class="language-plaintext highlighter-rouge">/-/status</code> separate is what stops someone
wiring a DB check into the liveness probe later because it was already there.</p>

<p>The exception is a per-instance dependency - a sidecar, a local cache, a node that cannot reach the DB while its peers
can. That is a genuine readiness case, but from inside the process it is indistinguishable from the dependency being
down for everyone. Only monitoring, which sees the whole fleet, can tell the two apart. So leave it out of readiness
and alert on it instead.</p>

<p>Which means that for a stateless service, readiness is mostly about <strong>lifecycle</strong>: the start and end of an instance’s
life, plus a handful of stateful special cases (leader election, rebalancing, deliberate withdrawal for debugging).</p>

<h3 id="one-port">One port</h3>

<p>A separate management port was for putting operability traffic on a different network interface, which meant something
on a rack server and means nothing in a pod or an <code class="language-plaintext highlighter-rouge">awsvpc</code> task, where every port shares one network namespace. Worse,
a separate connector usually has its own acceptor and thread pool, so it can answer “alive” while the listener that
actually takes traffic is deadlocked. Restrict access by path at the ingress instead.</p>

<h3 id="the-prefix">The prefix</h3>

<p>Group the endpoints under a prefix, so a single ingress rule covers them and they cannot collide with application
routes. Spring uses <code class="language-plaintext highlighter-rouge">/actuator</code>, a control-theory word for the part that acts on a system, which nobody outside Spring
recognises. Quarkus uses <code class="language-plaintext highlighter-rouge">/q</code>. Prometheus, Alertmanager, Thanos, Loki and GitLab use <code class="language-plaintext highlighter-rouge">/-/</code>, which is the one with
prior art outside a single framework.</p>

<h3 id="body-formats">Body formats</h3>

<p>There is no ratified standard for a health check body. The nearest is the <a href="https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check-06">IETF draft</a> “Health Check Response
Format for HTTP APIs”, which reached version 06 in 2021 and expired. It defines <code class="language-plaintext highlighter-rouge">application/health+json</code> with
<code class="language-plaintext highlighter-rouge">status</code> of <code class="language-plaintext highlighter-rouge">pass</code>, <code class="language-plaintext highlighter-rouge">warn</code> or <code class="language-plaintext highlighter-rouge">fail</code>, and a <code class="language-plaintext highlighter-rouge">checks</code> map of named sub-checks each with their own <code class="language-plaintext highlighter-rouge">status</code>,
<code class="language-plaintext highlighter-rouge">observedValue</code>, <code class="language-plaintext highlighter-rouge">observedUnit</code>, <code class="language-plaintext highlighter-rouge">time</code> and <code class="language-plaintext highlighter-rouge">output</code>. The media type was never registered with IANA, but libraries in
most languages implement the draft and it is the format people reach for. The alternative is Eclipse MicroProfile
Health, an actual spec, but it is <code class="language-plaintext highlighter-rouge">application/json</code> with <code class="language-plaintext highlighter-rouge">UP</code>/<code class="language-plaintext highlighter-rouge">DOWN</code> and no <code class="language-plaintext highlighter-rouge">warn</code>, and <code class="language-plaintext highlighter-rouge">warn</code> is exactly what
<code class="language-plaintext highlighter-rouge">/-/status</code> needs for pressure and dependency states.</p>

<p>All four health endpoints return the same shape: a <code class="language-plaintext highlighter-rouge">status</code> and the <code class="language-plaintext highlighter-rouge">checks</code> that produced it. They differ only in
which checks are included. <code class="language-plaintext highlighter-rouge">/-/live</code> folds the liveness checks, <code class="language-plaintext highlighter-rouge">/-/ready</code> the lifecycle checks, <code class="language-plaintext highlighter-rouge">/-/status</code>
everything, and <code class="language-plaintext highlighter-rouge">/-/up</code> is <code class="language-plaintext highlighter-rouge">/-/status</code> with <code class="language-plaintext highlighter-rouge">checks</code> omitted.
Including <code class="language-plaintext highlighter-rouge">checks</code> in the probe responses is not just for humans: the kubelet records the response body of a failed
<code class="language-plaintext highlighter-rouge">httpGet</code> probe in the pod’s events, so <code class="language-plaintext highlighter-rouge">kubectl describe pod</code> shows which check failed rather than just “503”.</p>

<h3 id="public-status">Public status</h3>

<p>An SLA needs a third-party monitor, and a third-party monitor is far easier to run against an unauthenticated URL.
But <code class="language-plaintext highlighter-rouge">/-/status</code> says which database is down and how full the heap is, which is not for strangers. Path-based
authorisation at the ingress can allow or deny a path; it cannot strip a body. So the public view has to be a second
path: <code class="language-plaintext highlighter-rouge">/-/up</code>, the same fold as <code class="language-plaintext highlighter-rouge">/-/status</code> with everything but the top-level <code class="language-plaintext highlighter-rouge">status</code> removed.</p>

<p>Anything public under <code class="language-plaintext highlighter-rouge">/-/</code> must not do work per request. An unauthenticated endpoint that pings the database on every
hit is an amplification vector. So checks run on a timer and the endpoints serve the latest snapshot, which is what the
draft’s per-check <code class="language-plaintext highlighter-rouge">time</code> field is for. That is good practice for the protected endpoints too.</p>

<p><code class="language-plaintext highlighter-rouge">/-/</code> is an index so the rest are discoverable, by tooling and by a human with <code class="language-plaintext highlighter-rouge">curl</code>. <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-11">HAL</a> is another expired
IETF draft, but it is what Spring’s <code class="language-plaintext highlighter-rouge">/actuator</code> index returns and every language has a client for it.</p>

<p>For metrics, <a href="https://github.com/prometheus/OpenMetrics/blob/main/specification/OpenMetrics.md">OpenMetrics</a> is the Prometheus exposition format formalised. Scrapers content-negotiate;
an old one will ask for <code class="language-plaintext highlighter-rouge">text/plain; version=0.0.4</code> and should get it.</p>

<h3 id="info-format">Info format</h3>

<p>There is no standard for what an info endpoint returns. Spring’s is freeform, populated by whatever contributors are on
the classpath. But there is a standard vocabulary for describing a running process: the OpenTelemetry Resource
<a href="https://opentelemetry.io/docs/specs/semconv/resource/">semantic conventions</a>. <code class="language-plaintext highlighter-rouge">service.name</code>, <code class="language-plaintext highlighter-rouge">service.version</code>, <code class="language-plaintext highlighter-rouge">service.instance.id</code>,
<code class="language-plaintext highlighter-rouge">deployment.environment.name</code>, <code class="language-plaintext highlighter-rouge">process.runtime.name</code>, <code class="language-plaintext highlighter-rouge">container.image.tags</code>, <code class="language-plaintext highlighter-rouge">k8s.pod.name</code>, <code class="language-plaintext highlighter-rouge">cloud.region</code>,
<code class="language-plaintext highlighter-rouge">vcs.ref.head.revision</code>. Every span and metric the process emits already carries these, so <code class="language-plaintext highlighter-rouge">/-/info</code> has a precise
definition: the Resource this process attaches to its telemetry. What it returns must match what you see in your
tracing UI, which is a useful thing to be able to check.</p>

<p>Serialise the SDK’s <code class="language-plaintext highlighter-rouge">Resource</code> with the dotted keys nested, which is how Datadog’s span overview displays them.
Nesting is safe because the conventions forbid a namespace also being a key. Two attributes to filter out:
<code class="language-plaintext highlighter-rouge">process.command_line</code> and <code class="language-plaintext highlighter-rouge">process.executable.path</code> are populated by the standard detectors and can leak secrets.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[A standard set of HTTP endpoints a service should expose for the infrastructure that runs it. Recommendations first, reasoning after.]]></summary></entry><entry><title type="html">Understanding K8S Ingresses &amp;amp; Load Balancer Controllers on AWS</title><link href="https://blog.lidalia.org.uk/2024/10/understanding-k8s-ingresses-&-load-balancer-controllers-on-aws.html" rel="alternate" type="text/html" title="Understanding K8S Ingresses &amp;amp; Load Balancer Controllers on AWS" /><published>2024-10-19T11:40:38+00:00</published><updated>2024-10-19T11:40:38+00:00</updated><id>https://blog.lidalia.org.uk/2024/10/understanding-k8s-ingresses-&amp;-load-balancer-controllers-on-aws</id><content type="html" xml:base="https://blog.lidalia.org.uk/2024/10/understanding-k8s-ingresses-&amp;-load-balancer-controllers-on-aws.html"><![CDATA[<p>Some notes from my recent battles with AWS NLBs &amp; the Ingress-Nginx Controller.</p>

<p>In order to expose a K8S cluster running on AWS EC2 instances (as an EKS cluster does) you will need an ELB in some
form to load balance across the EC2 instances hosting the cluster. The ELB can either <em>be</em> a K8S ingress or it can be
a route to the K8S ingress - that is, the actual ingress running inside the K8S cluster is exposed on ports on the
EC2 instances, and the ELB balances across those nodes.</p>

<h2 id="types-of-aws-load-balancer-controller">Types of AWS Load Balancer Controller</h2>

<p>There are two types of AWS Load Balancer Controller.</p>

<ol>
  <li>
    <p>The legacy in-tree controller</p>

    <p>This is baked into an EKS kubernetes cluster without any installation. You do not see any Pods or Service or Ingress
Class for it as resources inside the cluster.</p>

    <p>It will create an NLB for any K8S Service with <code class="language-plaintext highlighter-rouge">spec: type: LoadBalancer</code> which will proxy to that service.</p>

    <p>It is legacy and deprecated.</p>
  </li>
  <li>
    <p>The <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/">AWS Load Balancer Controller</a></p>

    <p>This is an external controller. It can be installed via helm. It will create resources (a Deployment, Pods, an
Ingress Class) in the cluster.</p>

    <p>It will create an NLB for any K8S Service with <code class="language-plaintext highlighter-rouge">spec: type: LoadBalancer</code> which will proxy to that service.</p>

    <p>It will create an ALB for any K8S Ingress with <code class="language-plaintext highlighter-rouge">spec: ingressClassName: alb</code>.</p>

    <p>It is the recommended way to integrate with AWS load balancers.</p>

    <p>Prior to v2 it was called the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v1.1/">AWS ALB Ingress Controller</a>
which is deprecated.</p>

    <p><a href="https://docs.aws.amazon.com/eks/latest/userguide/lbc-helm.html">Installation is documented here</a>.</p>
  </li>
</ol>

<h3 id="differences-between-the-controller-types">Differences between the Controller types</h3>

<p>Importantly, many of the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.0/guide/service/annotations/">documented annotations</a> are not supported by the legacy in-tree
controller. This includes the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#proxy-protocol-v2"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-proxy-protocol</code></a>
we will discuss below.</p>

<p>Some of the <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.0/guide/service/annotations/">documented annotations</a> have different default values. Notably
<a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#lb-scheme"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-scheme</code></a> (whether the provisioned ELB should be
<code class="language-plaintext highlighter-rouge">internal</code> or <code class="language-plaintext highlighter-rouge">internet-facing</code>) defaults to <code class="language-plaintext highlighter-rouge">internet-facing</code> on the AWS Load Balancer Controller but <code class="language-plaintext highlighter-rouge">internal</code> on the
legacy in-tree controller.</p>

<h2 id="the-ingress-nginx-controller">The Ingress-Nginx Controller</h2>

<p>The <a href="https://kubernetes.github.io/ingress-nginx/">Ingress-Nginx Controller</a> is probably the preferred K8S Ingress Class when running on AWS. It allows
you to manage your ingress costs inside the cluster, by making your K8S Services have <code class="language-plaintext highlighter-rouge">spec: type: ClusterIP</code> and
exposing them via an Ingress with <code class="language-plaintext highlighter-rouge">spec: ingressClassName: nginx</code>, whereas using a K8S Service with
<code class="language-plaintext highlighter-rouge">spec: type: LoadBalancer</code> or an Ingress with <code class="language-plaintext highlighter-rouge">spec: ingressClassName: alb</code> will create further costly AWS ELBs. A
single NLB can then act as the front door to the Ingress-Nginx Controller.</p>

<p>Installation of the Ingress-Nginx Controller creates, among other things, a K8S Service and Deployment. The K8S Service
will have <code class="language-plaintext highlighter-rouge">spec: type: LoadBalancer</code>, which on AWS with the AWS Load Balancer Controller installed will provision an NLB
to proxy traffic to the exposed port(s) on the EC2 instances, which in turn will forward that traffic to the nginx pods.</p>

<h3 id="configuring-the-provisioned-nlb">Configuring the provisioned NLB</h3>

<p>The nature of the provisioned NLB can be controlled by adding <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.0/guide/service/annotations/">AWS annotations</a> to the nginx K8S
Service. For our purposes important values are:</p>

<ul>
  <li>
    <p><a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#lb-scheme"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-scheme</code></a></p>

    <p>Not a URI scheme, instead whether the provisioned ELB should be <code class="language-plaintext highlighter-rouge">internal</code> or <code class="language-plaintext highlighter-rouge">internet-facing</code>.</p>
  </li>
  <li>
    <p><a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#ssl-cert"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-ssl-cert</code></a></p>

    <p>The ARN of an <a href="https://us-east-1.console.aws.amazon.com/acm/home">AWS Certificate Manager</a> TLS certificate that will be served by the NLB to allow it to serve
https requests.</p>
  </li>
  <li>
    <p><a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#proxy-protocol-v2"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'</code></a></p>

    <p>If present, the generated Target Groups will be configured to send the <a href="#proxy-protocol-v2">Proxy Protocol v2</a> header.</p>
  </li>
</ul>

<h3 id="installing-the-ingress-nginx-controller">Installing the Ingress-Nginx Controller</h3>

<h4 id="via-manifest">Via Manifest</h4>

<p>The Ingress-Nginx team <a href="https://kubernetes.github.io/ingress-nginx/deploy/#tls-termination-in-aws-load-balancer-nlb">provide a manifest for installing the controller with TLS terminated at the NLB</a>.
It requires setting a couple of values, documented in the linked installation instructions. However, it has several
quirks to be aware of:</p>

<ol>
  <li>
    <p>It does not enable <a href="#proxy-protocol-v2">Proxy Protocol v2</a> on either the NLB or nginx</p>

    <p>See <a href="#enabling-proxy-protocol-v2">Enabling Proxy Protocol v2</a></p>
  </li>
  <li>
    <p>It only creates one replica of the nginx pod</p>

    <p>As the pods are exposed on the EC2 instances forming the cluster’s nodes as the registered targets of the NLB’s
target group(s), this means that all but one of the registered targets will be considered unhealthy.</p>

    <p>This can be fixed by adding <code class="language-plaintext highlighter-rouge">spec: replicas: 2</code> (or however many EC2 nodes you have) to the Deployment manifest.</p>
  </li>
  <li>
    <p>It does not specify a value for <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#lb-scheme"><code class="language-plaintext highlighter-rouge">service.beta.kubernetes.io/aws-load-balancer-scheme</code></a></p>

    <p>So whether your NLB is internet facing will depend on the default, which <a href="#differences-between-the-controller-types">differs per AWS controller
type</a>.</p>

    <p>This can be fixed by adding <a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#lb-scheme"><code class="language-plaintext highlighter-rouge">metadata: annotation: service.beta.kubernetes.io/aws-load-balancer-scheme</code></a>
to the Service manifest.</p>
  </li>
  <li>
    <p>It takes control of redirecting <code class="language-plaintext highlighter-rouge">http</code> to <code class="language-plaintext highlighter-rouge">https</code>, preventing management of this at the Ingress resource level</p>

    <p><a href="#http-to-https-redirection">Full discussion below</a>.</p>
  </li>
</ol>

<h5 id="enabling-proxy-protocol-v2">Enabling Proxy Protocol v2</h5>

<p><a href="#proxy-protocol-v2">Proxy Protocol v2</a> can be enabled as so:</p>

<ol>
  <li>On the NLB side by adding
<a href="https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.9/guide/service/annotations/#proxy-protocol-v2"><code class="language-plaintext highlighter-rouge">metadata: annotation: service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'</code></a>
to the Service manifest</li>
  <li>On the nginx side by adding <code class="language-plaintext highlighter-rouge">data: use-proxy-protocol: "true"</code> to the ConfigMap manifest</li>
</ol>

<h5 id="http-to-https-redirection">http to https redirection</h5>

<p>The nlb-with-tls-termination manifest implements http to https redirection by exposing an extra port 2443 named
<code class="language-plaintext highlighter-rouge">tohttps</code> on the nginx Pods, adding a special listener on port 2443 to nginx via <code class="language-plaintext highlighter-rouge">data: http-snippet:</code> on the ConfigMap
that always returns a 308 to https, and then specifying that the http port on the Service manifest has a target port of
<code class="language-plaintext highlighter-rouge">tohttps</code>, which configures the NLB’s Target Group for port 80 to route traffic to port 2443 on the nginx pod
(ultimately - there is a further port mapping on the EC2 instance to get to 2443).</p>

<p>Given that it does not enable <a href="#proxy-protocol-v2">Proxy Protocol v2</a>, it makes sense for the manifest to do this, as
most services that serve over TLS will want a redirect from http to https and without the Proxy Protocol nginx cannot
know what the original protocol was. An NLB cannot do the redirect, so it has to happen at the nginx level.</p>

<p>This breaks if you enable <a href="#proxy-protocol-v2">Proxy Protocol v2</a>, because the <code class="language-plaintext highlighter-rouge">http-snippet</code> on the ConfigMap will not
be updated to expect the proxy protocol header despite adding <code class="language-plaintext highlighter-rouge">data: use-proxy-protocol: "true"</code> to the ConfigMap
resource.</p>

<p>Redirecting via a custom port is unnecessary if you have a <code class="language-plaintext highlighter-rouge">spec: tls: hosts</code> array on the Ingress resource <strong>AND</strong> you
have <a href="#proxy-protocol-v2">Proxy Protocol v2</a> enabled (so that nginx knows if the request were actually via https), as
that will configure nginx to send redirects from http to https for all rules in that Ingress resource by default, and
will also allow you to disable this behaviour per Ingress by adding
<code class="language-plaintext highlighter-rouge">metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "false"</code> to the Ingress resource.</p>

<p>The Ingress redirect only works if you specify a <code class="language-plaintext highlighter-rouge">spec: tls: hosts</code> array on the Ingress resource. I’m not sure if this
is an odd thing to do if TLS is being terminated downstream of the ingress at the NLB.</p>

<p>If you need precise control over which services have and do not have http to https redirects, it can be achieved with
the following steps:</p>

<ol>
  <li><a href="#enabling-proxy-protocol-v2">Enable Proxy Protocol v2</a>.</li>
  <li>Change the Service manifest - find the <code class="language-plaintext highlighter-rouge">spec: ports</code> port with name <code class="language-plaintext highlighter-rouge">http</code> and change its <code class="language-plaintext highlighter-rouge">targetPort</code> from <code class="language-plaintext highlighter-rouge">tohttps</code>
to <code class="language-plaintext highlighter-rouge">http</code>.</li>
  <li>Change the Deployment manifest - find the <code class="language-plaintext highlighter-rouge">spec: template: spec: containers</code> container with name <code class="language-plaintext highlighter-rouge">controller</code>. Delete
the port with name <code class="language-plaintext highlighter-rouge">tohttps</code>.</li>
  <li>Change the ConfigMap manifest - delete the <code class="language-plaintext highlighter-rouge">data: http-snippet</code>.</li>
  <li>Make any Ingress resources you have that should redirect from http to https have a <code class="language-plaintext highlighter-rouge">spec: tls: hosts</code> containing all
the hosts they serve, and <code class="language-plaintext highlighter-rouge">metadata: annotations: nginx.ingress.kubernetes.io/ssl-redirect: "false"</code> if they should
serve over http.</li>
</ol>

<p>Other options I have not yet explored in full:</p>

<ol>
  <li>Terminate TLS at nginx rather than the ELB. Then it makes sense to have <code class="language-plaintext highlighter-rouge">spec: tls: hosts</code> on the Ingress resources,
and there is no need to worry about Proxy Protocol v2. Requires getting the certificate into K8S so nginx can serve
it.</li>
  <li>Use an ALB rather than an NLB by adding <code class="language-plaintext highlighter-rouge">metadata: annotations: service.beta.kubernetes.io/aws-load-balancer-type: alb</code>
to the Service manifest. This should remove the need to have Proxy Protocol v2 enabled.</li>
</ol>

<h4 id="via-helm">Via Helm</h4>

<p>There is an <a href="https://kubernetes.github.io/ingress-nginx/deploy/#quick-start">Ingress-Nginx Controller Helm chart</a>. I have not experimented with it yet but it may
be possible to use it to avoid tweaking a downloaded manifest.</p>

<h2 id="proxy-protocol-v2">Proxy Protocol v2</h2>

<p>Typically, a network layer 4 device like an NLB cannot provide any further information to the upstream services to which
it proxies. The bits are simply sent “as is”. This is a problem with TLS terminated at the NLB, because a typical
HTTP request does not contain the requested scheme within the body of the request. Consequently, the fact that the
original scheme was <code class="language-plaintext highlighter-rouge">https</code> is lost to upstream services. Upstream services may need to construct URLs with that
knowledge.</p>

<p>The <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">Proxy Protocol v2</a> specifies a way for Layer 4 devices like an NLB to provide proxy information to
upstream services via a header sent <em>before</em> the rest of the request (including before the request line). The <a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#target-group-attributes">Target
Groups for AWS NLBs can be configured to send it</a> (in the console, edit the Target Group’s
“Attributes”), and <a href="https://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_protocol">nginx can be configured to expect it</a>.</p>

<p>Ideally applications do not need this information - they should return links as URI references rather than URLs, either
omitting the scheme (e.g. <code class="language-plaintext highlighter-rouge">//example.com/my/thing</code>) or the entire authority (e.g. <code class="language-plaintext highlighter-rouge">/my/thing</code>). However, some frameworks
insist on returning URLs for e.g. the <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-location"><code class="language-plaintext highlighter-rouge">Location</code> header</a>, reconstructing the scheme from the
<code class="language-plaintext highlighter-rouge">X-Forwarded-Proto</code> or <a href="https://www.rfc-editor.org/rfc/rfc7239.html"><code class="language-plaintext highlighter-rouge">Forwarded</code> header</a>, falling back on the scheme the process is listening for,
because previous (obsolete) versions of the HTTP specification
<a href="https://www.rfc-editor.org/rfc/rfc2616#section-14.30">required the <code class="language-plaintext highlighter-rouge">Location</code> header to contain a URL</a>.</p>

<p>If http to https redirection is expected to happen upstream of an NLB then proxy information will be required.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[Some notes from my recent battles with AWS NLBs &amp; the Ingress-Nginx Controller.]]></summary></entry><entry><title type="html">Working with Clikt</title><link href="https://blog.lidalia.org.uk/2023/06/working-with-clikt.html" rel="alternate" type="text/html" title="Working with Clikt" /><published>2023-06-20T09:06:04+00:00</published><updated>2023-06-20T09:06:04+00:00</updated><id>https://blog.lidalia.org.uk/2023/06/working-with-clikt</id><content type="html" xml:base="https://blog.lidalia.org.uk/2023/06/working-with-clikt.html"><![CDATA[<p>Couple of quick notes on working with <a href="https://ajalt.github.io/clikt/">Clikt</a>,
the Kotlin CLI library:</p>

<h2 id="using-clikt-as-a-pure-parser">Using Clikt as a pure parser</h2>

<p>If all you want is to turn command line arguments into a data class, without
using the Clikt Command abstraction, you can do it as follows:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">com.github.ajalt.clikt.core.NoOpCliktCommand</span>
<span class="k">import</span> <span class="nn">com.github.ajalt.clikt.parameters.options.option</span>
<span class="k">import</span> <span class="nn">com.github.ajalt.clikt.parameters.options.required</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">(</span><span class="k">vararg</span> <span class="n">args</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">val</span> <span class="py">config</span><span class="p">:</span> <span class="nc">Config</span> <span class="p">=</span> <span class="nc">CliParser</span><span class="p">.</span><span class="nf">parseConfig</span><span class="p">(*</span><span class="n">args</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">data class</span> <span class="nc">Config</span><span class="p">(</span><span class="kd">val</span> <span class="py">username</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="kd">class</span> <span class="nc">CliParser</span> <span class="k">private</span> <span class="k">constructor</span><span class="p">()</span> <span class="p">:</span> <span class="nc">NoOpCliktCommand</span><span class="p">(</span>
  <span class="n">name</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
  <span class="n">help</span> <span class="p">=</span> <span class="s">"help text"</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>

  <span class="k">private</span> <span class="kd">val</span> <span class="py">username</span><span class="p">:</span> <span class="nc">String</span> <span class="k">by</span> <span class="nf">option</span><span class="p">(</span><span class="n">help</span> <span class="p">=</span> <span class="s">"the username"</span><span class="p">).</span><span class="nf">required</span><span class="p">()</span>

  <span class="k">private</span> <span class="k">fun</span> <span class="nf">toConfig</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Config</span><span class="p">(</span><span class="n">username</span> <span class="p">=</span> <span class="n">username</span><span class="p">)</span>

  <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>

    <span class="k">fun</span> <span class="nf">parseConfig</span><span class="p">(</span><span class="k">vararg</span> <span class="n">args</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Config</span> <span class="p">=</span>
      <span class="nc">CliParser</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">parse</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="nf">toList</span><span class="p">())</span> <span class="p">}</span>
        <span class="p">.</span><span class="nf">toConfig</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="injecting-the-environment-as-a-map">Injecting the environment as a map</h2>

<p>If you want to decouple Clikt from <code class="language-plaintext highlighter-rouge">System.getenv</code> you can do so as so:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">CliParser</span><span class="p">(</span>
  <span class="n">env</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">CliktCommand</span> <span class="p">{</span>

  <span class="nf">init</span> <span class="p">{</span>
    <span class="nf">context</span> <span class="p">{</span>
      <span class="n">envarReader</span> <span class="p">=</span> <span class="n">env</span><span class="o">::</span><span class="k">get</span>
      <span class="n">autoEnvvarPrefix</span> <span class="p">=</span> <span class="s">"MY_APP"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="putting-them-together">Putting them together</h2>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nn">com.github.ajalt.clikt.core.NoOpCliktCommand</span>
<span class="k">import</span> <span class="nn">com.github.ajalt.clikt.parameters.options.option</span>
<span class="k">import</span> <span class="nn">com.github.ajalt.clikt.parameters.options.required</span>

<span class="k">fun</span> <span class="nf">main</span><span class="p">(</span><span class="k">vararg</span> <span class="n">args</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">config</span><span class="p">:</span> <span class="nc">Config</span> <span class="p">=</span> <span class="nc">CliParser</span><span class="p">.</span><span class="nf">parseConfig</span><span class="p">(*</span><span class="n">args</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">data class</span> <span class="nc">Config</span><span class="p">(</span><span class="kd">val</span> <span class="py">username</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span>

<span class="kd">class</span> <span class="nc">CliParser</span> <span class="k">private</span> <span class="k">constructor</span><span class="p">(</span>
  <span class="n">env</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">NoOpCliktCommand</span><span class="p">(</span>
  <span class="n">name</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
  <span class="n">help</span> <span class="p">=</span> <span class="s">"help text"</span><span class="p">,</span>
<span class="p">)</span> <span class="p">{</span>

  <span class="nf">init</span> <span class="p">{</span>
    <span class="nf">context</span> <span class="p">{</span>
      <span class="n">envarReader</span> <span class="p">=</span> <span class="n">env</span><span class="o">::</span><span class="k">get</span>
      <span class="n">autoEnvvarPrefix</span> <span class="p">=</span> <span class="s">"MY_APP"</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="kd">val</span> <span class="py">username</span><span class="p">:</span> <span class="nc">String</span> <span class="k">by</span> <span class="nf">option</span><span class="p">(</span><span class="n">help</span> <span class="p">=</span> <span class="s">"the username"</span><span class="p">).</span><span class="nf">required</span><span class="p">()</span>

  <span class="k">private</span> <span class="k">fun</span> <span class="nf">toConfig</span><span class="p">()</span> <span class="p">=</span> <span class="nc">Config</span><span class="p">(</span><span class="n">username</span> <span class="p">=</span> <span class="n">username</span><span class="p">)</span>

  <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>

      <span class="k">fun</span> <span class="nf">parseConfig</span><span class="p">(</span>
          <span class="n">args</span><span class="p">:</span> <span class="nc">List</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">&gt;,</span>
          <span class="n">env</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(),</span>
      <span class="p">):</span> <span class="nc">Config</span> <span class="p">=</span>
          <span class="nc">CliParser</span><span class="p">(</span><span class="n">env</span><span class="p">)</span>
              <span class="p">.</span><span class="nf">apply</span> <span class="p">{</span> <span class="nf">parse</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="p">}</span>
              <span class="p">.</span><span class="nf">toConfig</span><span class="p">()</span>

    <span class="k">fun</span> <span class="nf">parseConfig</span><span class="p">(</span>
        <span class="k">vararg</span> <span class="n">args</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
        <span class="n">env</span><span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">String</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">System</span><span class="p">.</span><span class="nf">getenv</span><span class="p">(),</span> 
    <span class="p">):</span> <span class="nc">Config</span> <span class="p">=</span> <span class="nf">parseConfig</span><span class="p">(</span>
        <span class="n">args</span><span class="p">.</span><span class="nf">toList</span><span class="p">(),</span> 
        <span class="n">env</span><span class="p">,</span>
    <span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Robert Elliot</name></author><category term="clikt" /><category term="kotlin" /><summary type="html"><![CDATA[Couple of quick notes on working with Clikt, the Kotlin CLI library:]]></summary></entry><entry><title type="html">Maps with typed keys in Kotlin</title><link href="https://blog.lidalia.org.uk/2023/02/maps-with-typed-keys-in-kotlin.html" rel="alternate" type="text/html" title="Maps with typed keys in Kotlin" /><published>2023-02-03T08:42:50+00:00</published><updated>2023-02-03T08:42:50+00:00</updated><id>https://blog.lidalia.org.uk/2023/02/maps-with-typed-keys-in-kotlin</id><content type="html" xml:base="https://blog.lidalia.org.uk/2023/02/maps-with-typed-keys-in-kotlin.html"><![CDATA[<p><a href="https://medium.com/@sam-cooper">Sam Cooper</a> showed me on the Kotlin Slack that
you can create an immutable-ish class that is both a <code class="language-plaintext highlighter-rouge">Map&lt;String, T&gt;</code> and has
<code class="language-plaintext highlighter-rouge">val</code> properties representing compile time enforced keys on that <code class="language-plaintext highlighter-rouge">Map</code>, and
without much duplication.</p>

<p>Edit 2023-02-06 - we’ve made a couple of improvements:</p>

<ul>
  <li>It’s now invariant in <code class="language-plaintext highlighter-rouge">V</code>, the type of the values, so an
<code class="language-plaintext highlighter-rouge">AbstractPropertyMap&lt;String&gt;</code> is a subtype of
<code class="language-plaintext highlighter-rouge">AbstractPropertyMap&lt;CharSequence&gt;</code></li>
  <li>The type of the properties is now captured by the type of the argument to
<code class="language-plaintext highlighter-rouge">property</code>, so in an <code class="language-plaintext highlighter-rouge">AbstractPropertyMap&lt;CharSequence&gt;</code>
<code class="language-plaintext highlighter-rouge">val aString by property("a string")</code> will have its type inferred as
<code class="language-plaintext highlighter-rouge">String</code>. (This is obviously useful if extending  <code class="language-plaintext highlighter-rouge">AbstractPropertyMap&lt;Any&gt;</code>.)</li>
</ul>

<p>Create this abstract class:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">abstract</span> <span class="kd">class</span> <span class="nc">AbstractPropertyMap</span><span class="p">&lt;</span><span class="k">out</span> <span class="nc">V</span><span class="p">&gt;(</span>
  <span class="k">private</span> <span class="kd">val</span> <span class="py">properties</span><span class="p">:</span> <span class="nc">MutableMap</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">V</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nf">mutableMapOf</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">Map</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">V</span><span class="p">&gt;</span> <span class="k">by</span> <span class="nf">properties</span> <span class="p">{</span>
  <span class="k">protected</span> <span class="k">fun</span> <span class="p">&lt;</span><span class="nc">V2</span> <span class="p">:</span> <span class="err">@</span><span class="nc">UnsafeVariance</span> <span class="nc">V</span><span class="p">&gt;</span> <span class="nf">property</span><span class="p">(</span><span class="n">initialValue</span><span class="p">:</span> <span class="nc">V2</span><span class="p">)</span> <span class="p">=</span>
    <span class="nc">PropertyDelegateProvider</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">,</span> <span class="nc">ReadOnlyProperty</span><span class="p">&lt;</span><span class="nc">Any</span><span class="p">,</span> <span class="nc">V2</span><span class="p">&gt;&gt;</span> <span class="p">{</span> <span class="n">_</span><span class="p">,</span> <span class="n">prop</span> <span class="p">-&gt;</span>
      <span class="n">properties</span><span class="p">[</span><span class="n">prop</span><span class="p">.</span><span class="n">name</span><span class="p">]</span> <span class="p">=</span> <span class="n">initialValue</span>
      <span class="nc">ReadOnlyProperty</span><span class="p">(</span><span class="n">properties</span><span class="o">::</span><span class="n">getValue</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can then create subclasses as so:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Links</span><span class="p">(</span>
  <span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
  <span class="n">otherId</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">AbstractPropertyMap</span><span class="p">&lt;</span><span class="nc">URI</span><span class="p">&gt;()</span> <span class="p">{</span>
  <span class="kd">val</span> <span class="py">self</span> <span class="k">by</span> <span class="nf">property</span><span class="p">(</span><span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/$id"</span><span class="p">))</span>
  <span class="kd">val</span> <span class="py">other</span> <span class="k">by</span> <span class="nf">property</span><span class="p">(</span><span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/other/$otherId"</span><span class="p">))</span>
<span class="p">}</span>

<span class="kd">val</span> <span class="py">links</span> <span class="p">=</span> <span class="nc">Links</span><span class="p">(</span><span class="s">"1"</span><span class="p">,</span> <span class="s">"2"</span><span class="p">)</span>

<span class="n">links</span><span class="p">.</span><span class="n">self</span> <span class="p">==</span> <span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/1"</span><span class="p">)</span>
<span class="n">links</span><span class="p">[</span><span class="s">"self"</span><span class="p">]</span> <span class="p">==</span> <span class="n">links</span><span class="p">.</span><span class="n">self</span>

<span class="n">links</span><span class="p">.</span><span class="n">other</span> <span class="p">==</span> <span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/other/2"</span><span class="p">)</span>
<span class="n">links</span><span class="p">[</span><span class="s">"other"</span><span class="p">]</span> <span class="p">==</span> <span class="n">links</span><span class="p">.</span><span class="n">other</span>

<span class="n">links</span><span class="p">.</span><span class="n">keys</span> <span class="p">==</span> <span class="nf">setOf</span><span class="p">(</span><span class="s">"self"</span><span class="p">,</span> <span class="s">"other"</span><span class="p">)</span>
<span class="n">links</span><span class="p">.</span><span class="n">values</span><span class="p">.</span><span class="nf">toList</span><span class="p">()</span> <span class="p">==</span> <span class="nf">listOf</span><span class="p">(</span><span class="n">links</span><span class="p">.</span><span class="n">self</span><span class="p">,</span> <span class="n">links</span><span class="p">.</span><span class="n">other</span><span class="p">)</span>
<span class="n">links</span><span class="p">.</span><span class="n">entries</span> <span class="p">==</span> <span class="nf">setOf</span><span class="p">(</span>
  <span class="nc">SimpleImmutableEntry</span><span class="p">(</span><span class="s">"self"</span><span class="p">,</span> <span class="n">links</span><span class="p">.</span><span class="n">self</span><span class="p">),</span>
  <span class="nc">SimpleImmutableEntry</span><span class="p">(</span><span class="s">"other"</span><span class="p">,</span> <span class="n">links</span><span class="p">.</span><span class="n">other</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>Which begs the question - why?</p>

<p>I think the resulting class has a few nice properties:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Links</code> <em>is</em> a <code class="language-plaintext highlighter-rouge">Map&lt;String, URI&gt;</code>, so you can retrieve values with keys only
known at runtime and iterate over its entry set / key set / values without
needing reflective access.</li>
  <li>You only specify the key names once - no duplication, no room for error.</li>
  <li>The compiler forces you to instantiate <code class="language-plaintext highlighter-rouge">Links</code> with all the expected keys.</li>
  <li>The compiler enforces the type of all the properties created by delegation</li>
  <li>You have type safe access to the properties on the resulting instance</li>
</ol>

<p>In contrast, a <code class="language-plaintext highlighter-rouge">mapOf&lt;String, URI&gt;()</code> lacks 3 &amp; 5. A simple class lacks 1 &amp; 4.</p>

<p>A combination of the two could be made without the delegation magic, but would
be more noisy and require duplicating key names:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">Links</span><span class="p">(</span>
    <span class="n">id</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
    <span class="n">otherId</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">AbstractMap</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">URI</span><span class="p">&gt;()</span> <span class="p">{</span>
    <span class="kd">val</span> <span class="py">self</span> <span class="p">=</span> <span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/$id"</span><span class="p">)</span>
    <span class="kd">val</span> <span class="py">other</span> <span class="p">=</span> <span class="nc">URI</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="s">"/v1/other/$otherId"</span><span class="p">)</span>

    <span class="k">override</span> <span class="kd">val</span> <span class="py">entries</span><span class="p">:</span> <span class="nc">Set</span><span class="p">&lt;</span><span class="nc">Map</span><span class="p">.</span><span class="nc">Entry</span><span class="p">&lt;</span><span class="nc">String</span><span class="p">,</span> <span class="nc">URI</span><span class="p">&gt;&gt;</span> <span class="p">=</span> <span class="nf">setOf</span><span class="p">(</span>
        <span class="nc">AbstractMap</span><span class="p">.</span><span class="nc">SimpleImmutableEntry</span><span class="p">(</span><span class="s">"self"</span><span class="p">,</span> <span class="n">self</span><span class="p">),</span>
        <span class="nc">AbstractMap</span><span class="p">.</span><span class="nc">SimpleImmutableEntry</span><span class="p">(</span><span class="s">"other"</span><span class="p">,</span> <span class="n">other</span><span class="p">),</span>
    <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[Sam Cooper showed me on the Kotlin Slack that you can create an immutable-ish class that is both a Map&lt;String, T&gt; and has val properties representing compile time enforced keys on that Map, and without much duplication.]]></summary></entry><entry><title type="html">GitHub Actions authorizing to AWS</title><link href="https://blog.lidalia.org.uk/2022/12/github-actions-authorizing-to-aws.html" rel="alternate" type="text/html" title="GitHub Actions authorizing to AWS" /><published>2022-12-28T11:09:01+00:00</published><updated>2022-12-28T11:09:01+00:00</updated><id>https://blog.lidalia.org.uk/2022/12/github-actions-authorizing-to-aws</id><content type="html" xml:base="https://blog.lidalia.org.uk/2022/12/github-actions-authorizing-to-aws.html"><![CDATA[<p>Quick notes on integrating GitHub Actions with AWS. Mostly to try and distill
the GitHub documentation into its essentials.</p>

<p>Fuller details can be found in GitHub’s documentation 
<a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect">About security hardening with OpenID Connect</a>
and specifically for AWS at <a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services">Configuring OpenID Connect in Amazon Web Services</a></p>

<h2 id="steps">Steps:</h2>

<h3 id="in-aws-iam">In AWS IAM</h3>

<p>1) Add an Identity Provider in AWS IAM</p>
<ul>
  <li>Provider: <code class="language-plaintext highlighter-rouge">token.actions.githubusercontent.com</code></li>
  <li>Audience: <code class="language-plaintext highlighter-rouge">sts.amazonaws.com</code></li>
</ul>

<p>2) Create an IAM Role</p>

<p>3) Under <strong>Permissions</strong> add a policy granting whatever it is the role needs to do
   (can be an inline policy, whatever - this is a standard IAM role, specific to
   what you need to do, nothing to do with it being GitHub Actions that will do
   it).</p>

<p>4) Under <strong>Trust relationships</strong> add something like this:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
          </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"Principal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"Federated"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;ARN of the token.actions.githubusercontent.com Identity Provider&gt;"</span><span class="w">
          </span><span class="p">},</span><span class="w">
          </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sts:AssumeRoleWithWebIdentity"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"Condition"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"StringEquals"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"token.actions.githubusercontent.com:aud"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sts.amazonaws.com"</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"StringLike"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"token.actions.githubusercontent.com:sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"repo:&lt;GitHub org or username&gt;/*"</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">StringLike</code> match is important - without it anyone who knows the ARN of
   the role could configure GitHub to authenticate as that role, but as they
   cannot change the <code class="language-plaintext highlighter-rouge">sub</code> that GitHub sends to AWS to authenticate you have
   complete control here of which repositories and branches / tags can assume
   this role. See <a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#configuring-the-oidc-trust-with-the-cloud">Configuring the OIDC trust with the cloud</a>.</p>

<h3 id="in-github-action">In GitHub Action</h3>

<p>1) Allow <code class="language-plaintext highlighter-rouge">id_token</code> write permission</p>

<p>By default the GitHub Action cannot write an id_token, so you need (at a
   minimum) this at the top of your workflow file:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="na">permissions</span><span class="pi">:</span>
     <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>
     <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
</code></pre></div></div>
<p><strong>You may need further permissions for other actions!</strong> See
   <a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings">Adding permissions settings</a> for more details / examples.</p>

<p>2) Add the following step to your job:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">configure aws credentials</span>
     <span class="na">uses</span><span class="pi">:</span> <span class="s">aws-actions/configure-aws-credentials@v1</span>
     <span class="na">with</span><span class="pi">:</span>
       <span class="na">role-to-assume</span><span class="pi">:</span> <span class="s">&lt;ARN of the Role you created above&gt;</span>
       <span class="na">aws-region</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>And that’s it, really.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[Quick notes on integrating GitHub Actions with AWS. Mostly to try and distill the GitHub documentation into its essentials.]]></summary></entry><entry><title type="html">Running Tests in Kubernetes</title><link href="https://blog.lidalia.org.uk/2022/03/running-tests-in-kubernetes.html" rel="alternate" type="text/html" title="Running Tests in Kubernetes" /><published>2022-03-28T14:53:34+00:00</published><updated>2022-03-28T14:53:34+00:00</updated><id>https://blog.lidalia.org.uk/2022/03/running-tests-in-kubernetes</id><content type="html" xml:base="https://blog.lidalia.org.uk/2022/03/running-tests-in-kubernetes.html"><![CDATA[<p>Recently I have had cause to run tests in Kubernetes via a GitHub Action, and 
it’s a <em>huge</em> pain so capturing it here.</p>

<p>I’ve shown it as a <code class="language-plaintext highlighter-rouge">gradle test</code> run but it should be more widely applicable.</p>

<p>Nice thing about is is that by mounting the local directory the whole way
through when the files are generated they are generated where you would expect
to see them locally.</p>

<h2 id="basics">Basics:</h2>
<ol>
  <li>GitHub Action steps
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">start minikube</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">medyagh/setup-minikube@latest</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">K8S Tests</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">run-k8s-tests.sh</span>
</code></pre></div>    </div>
  </li>
  <li>run-k8s-tests.sh
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>

<span class="nb">set</span> <span class="nt">-exuo</span> pipefail
   
main<span class="o">()</span> <span class="o">{</span>
  <span class="nb">trap</span> <span class="s1">'cleanup'</span> EXIT

  kubectl config use-context minikube
   
  ensure_a_clean_namespace

  prepare_minikube

  kubectl apply <span class="se">\</span>
    <span class="nt">-f</span> other-resources.yml <span class="se">\</span>
    <span class="nt">-f</span> k8s-tests.yml

  wait_for_tests_to_finish
<span class="o">}</span>

ensure_a_clean_namespace<span class="o">()</span> <span class="o">{</span>
  kubectl delete all <span class="nt">--all</span> <span class="nt">-n</span> k8s-test
<span class="o">}</span>
   
prepare_minikube<span class="o">()</span> <span class="o">{</span>
  minikube mount .:/home/gradle/project &amp;
<span class="o">}</span>

wait_for_tests_to_finish<span class="o">()</span> <span class="o">{</span>
  <span class="c"># Wait for the job to start - otherwise there will be no pods</span>
  kubectl <span class="nb">wait</span> <span class="nt">--for</span><span class="o">=</span><span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.status.active}'</span><span class="o">=</span>1 job/k8s-tests <span class="nt">--timeout</span><span class="o">=</span>30s <span class="nt">-n</span> k8s-test
   
  <span class="c"># Wait for the pods to be ready - be aware if the jobs is really fast this</span>
  <span class="c"># will fail because ready will go from true to false too fast for the check</span>
  <span class="c"># I cannot find a truly reliable of checking whether the pods *have* started in the past</span>
  kubectl <span class="nb">wait</span> <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span>ready pods <span class="nt">--selector</span><span class="o">=</span>job-name<span class="o">=</span>k8s-tests <span class="nt">--timeout</span><span class="o">=</span>30s <span class="nt">-n</span> k8s-test

  <span class="c"># Follow the logs to get real time info on what is happening</span>
  kubectl logs job/k8s-tests <span class="nt">--follow</span> <span class="nt">-n</span> k8s-test

  <span class="c"># The pods should be back with ready=false when they exit</span>
  kubectl <span class="nb">wait</span> <span class="nt">--for</span><span class="o">=</span><span class="nv">condition</span><span class="o">=</span><span class="nv">ready</span><span class="o">=</span><span class="nb">false </span>pods <span class="nt">--selector</span><span class="o">=</span>job-name<span class="o">=</span>k8s-tests <span class="nt">--timeout</span><span class="o">=</span>30s <span class="nt">-n</span> k8s-test
  <span class="c"># Meaning this should work</span>
  <span class="nb">local </span>exit_code<span class="p">;</span> <span class="nv">exit_code</span><span class="o">=</span><span class="si">$(</span>kubectl get pods <span class="nt">--selector</span><span class="o">=</span>job-name<span class="o">=</span>k8s-tests <span class="nt">--output</span><span class="o">=</span><span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].status.containerStatuses[0].state.terminated.exitCode}'</span> <span class="nt">-n</span> k8s-test<span class="si">)</span>
  <span class="k">return</span> <span class="s2">"</span><span class="nv">$exit_code</span><span class="s2">"</span>
<span class="o">}</span>
   
cleanup<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span>running_jobs<span class="p">;</span> <span class="nv">running_jobs</span><span class="o">=</span><span class="si">$(</span><span class="nb">jobs</span> <span class="nt">-p</span><span class="si">)</span>
  <span class="c"># shellcheck disable=SC2086</span>
  <span class="nb">kill</span> <span class="nv">$running_jobs</span> <span class="o">||</span> <span class="nb">true</span>
  <span class="c"># shellcheck disable=SC2086</span>
  <span class="nb">wait</span>
<span class="o">}</span>

main <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
</code></pre></div>    </div>
  </li>
  <li>k8s-tests.yml
    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">batch/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Job</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">k8s-tests</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">parallelism</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">completions</span><span class="pi">:</span> <span class="m">1</span>
  <span class="na">backoffLimit</span><span class="pi">:</span> <span class="m">0</span>
  <span class="c1"># Set this to something appropriate!</span>
  <span class="na">activeDeadlineSeconds</span><span class="pi">:</span> <span class="m">600</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">k8s-tests</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">gradle:7.4.0-jdk17</span>
          <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">gradle"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">test"</span><span class="pi">]</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/home/gradle/project</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">gradle-project</span>
      <span class="na">restartPolicy</span><span class="pi">:</span> <span class="s">Never</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">gradle-project</span>
          <span class="na">hostPath</span><span class="pi">:</span>
            <span class="na">path</span><span class="pi">:</span> <span class="s">/home/gradle/project</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Nice feature - <code class="language-plaintext highlighter-rouge">run-k8s-tests.sh</code> will run quite happily locally if you have
minikube &amp; bash installed.</p>

<p>The meat of the complication is the <code class="language-plaintext highlighter-rouge">wait_for_tests_to_finish</code> bash function.</p>

<p>If you are testing an image you are building, you can build and run it the tests
fairly efficiently in minikube as so:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nb">eval</span> <span class="s2">"</span><span class="si">$(</span>minikube docker-env<span class="si">)</span><span class="s2">"</span>

docker buildx build <span class="nb">.</span> <span class="nt">-t</span> myimage:local

./run-k8s-tests.sh
</code></pre></div></div>

<p>(I’m using buildx build here, but if you don’t need buildx a docker build will
do.)</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[Recently I have had cause to run tests in Kubernetes via a GitHub Action, and it’s a huge pain so capturing it here.]]></summary></entry><entry><title type="html">Keep Work Small &amp;amp; Short-lived</title><link href="https://blog.lidalia.org.uk/2022/02/keep-work-small-&-short-lived.html" rel="alternate" type="text/html" title="Keep Work Small &amp;amp; Short-lived" /><published>2022-02-26T11:56:12+00:00</published><updated>2022-02-26T11:56:12+00:00</updated><id>https://blog.lidalia.org.uk/2022/02/keep-work-small-&amp;-short-lived</id><content type="html" xml:base="https://blog.lidalia.org.uk/2022/02/keep-work-small-&amp;-short-lived.html"><![CDATA[<p>I’ve been mulling some agile ideas. Specifically, trunk based development and a
zero bug / no bug-database policy.</p>

<p>I’ve experienced both - I was on a project with upwards of 15 devs, doing
routine TDD on trunk with a zero bug policy - they were raised as physical pink
index cards on a physical board, and always went to the top, so there were the
next bit of work picked up.</p>

<p>My insight is that actually we <em>were</em> bug tracking with a bug database. The bug
database was the physical cards. And we had bugs, so it wasn’t a zero bugs
policy. What it was, was a <em>very short-lived</em> bug policy - we tolerated &amp;
tracked them for a very short period of time.</p>

<p>And the same was true of our trunk based development - we did branch. As soon as
a pair diverged from trunk on their work station, we had a branch, just locally
on that work station - and it needed to be merged by pushing to trunk (and
sometimes, of course, that caused merge conflicts that had to be resolved). But
crucially, those branches were <em>very short-lived</em>. Because they only existed
locally we expected to merge them into trunk multiple times a day.</p>

<p>My theory is that the two practises are actually not in themselves the silver
bullet, and indeed may represent local maxima. The real benefits are branches
and tracked bugs that are short-lived. The pros of the TBD &amp; zero bugs
practises is that it makes it very difficult <em>not</em> to have those benefits. If
you track your bugs as physical index cards on a physical board they can’t live
for long - you’ll have to fix them or throw them away. If you only branch onto
local machines no-one else can cherry-pick or merge in your changes, and you’re
in danger of losing them to some hardware failure, so there’s a huge incentive
to get your stuff on trunk ASAP.</p>

<p>However, if you can find a way to have those benefits whilst having remote
branches &amp; keeping bugs in a tracking system you may be able to get other
benefits. I like a protected trunk/main branch - when doing TBD the build got
broken from time to time, and it held everyone else up (the classic 5:30 cowboy
check-in &amp; run…). Set the CI up to auto-merge on successful build and you
mitigate that a lot without adding much overhead - provided you keep the branch
short-lived.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[I’ve been mulling some agile ideas. Specifically, trunk based development and a zero bug / no bug-database policy.]]></summary></entry><entry><title type="html">Log4Shell &amp;amp; small modules</title><link href="https://blog.lidalia.org.uk/2021/12/log4shell-&-small-modules.html" rel="alternate" type="text/html" title="Log4Shell &amp;amp; small modules" /><published>2021-12-30T12:29:18+00:00</published><updated>2021-12-30T12:29:18+00:00</updated><id>https://blog.lidalia.org.uk/2021/12/log4shell-&amp;-small-modules</id><content type="html" xml:base="https://blog.lidalia.org.uk/2021/12/log4shell-&amp;-small-modules.html"><![CDATA[<p>TLDR: if capabilities are kept in separate modules it mitigates the risk of a
capability being compromised.</p>

<p>Everyone’s talking about Log4Shell. A lot of the talk is, of course, about the
nature of code injection attacks, and the failure to separate code and data.
It’s an area where I feel sympathy for the Log4J 2 devs - I think the mistake
they made is a terrifyingly easy one to make, as every SQL injection or naked
<code class="language-plaintext highlighter-rouge">eval</code> regularly demonstrates. Layers of abstraction can lead to forgetting when
an input to a function is now untrusted data.</p>

<p>However, I think there’s another aspect of it that deserves consideration - the
fact that the ability to do this was installed on so many systems.</p>

<p>How many systems need to do LDAP JNDI lookups at all? I’m guessing a pretty
small percentage. How many need to do so from their logging system? A smaller
percentage. And of those, how many need to load complex classes (with static
initialisation) from a remote location via their logging system? I’m guessing an
absolutely <em>tiny</em> fraction of the systems vulnerable to Log4shell were actually
benefiting from the feature(s) that caused the vulnerability.</p>

<p>And yet there the code was, sitting on all those systems, waiting for the moment
someone found the vulnerability.</p>

<p>I’ve been experimenting with JPMS and jlink, and if you create a JRE without the
<code class="language-plaintext highlighter-rouge">java.naming</code> module then, unsurprisingly, you aren’t vulnerable to Log4Shell
even if you’re running an old Log4J 2 version. But inertia and a reasonably high
barrier to entry means that adoption of JPMS in general, and adoption of jlink
cut down JREs in particular, has been pretty minimal, unfortunately. And some of
the <code class="language-plaintext highlighter-rouge">java</code> namespaced modules are still disconcertingly enormous - for instance
using Java Beans (as lots of things, including Log4J 2, require) means bringing
in <code class="language-plaintext highlighter-rouge">java.desktop</code>, which brings in the <code class="language-plaintext highlighter-rouge">swing</code>, <code class="language-plaintext highlighter-rouge">awt</code> &amp; <code class="language-plaintext highlighter-rouge">applet</code> packages
amongst others. Perhaps rather more than you expected to call <code class="language-plaintext highlighter-rouge">set</code> on some data
class…</p>

<p>However, this could have been managed at the Log4J 2 level. <code class="language-plaintext highlighter-rouge">JndiManager</code> is in
the <code class="language-plaintext highlighter-rouge">org.apache.logging.log4j.core.net</code> package in the <code class="language-plaintext highlighter-rouge">log4j-core</code> jar, along
with rather a lot of things capable of doing I/O over multiple protocols. So you
can’t use Log4J 2 without having this logic installed. Had JNDI lookups been in
its own little jar, brought in as a separate dependency, the bug would still
have been in that code - but I’d hazard a guess that it would have been
drastically mitigated because most systems would not have had it sitting there,
a little otherwise pointless time bomb waiting to explode.</p>

<p>There’s a tension here with ease of use, and particularly keeping a low
barrier to entry. It <em>is</em> nice when it turns out you can just use a feature
without scrabbling around trying to work out which dependency you need to add to
get it to work. But having so many features lying around unused can have a high
price.</p>

<p>Keep it small. Keep it focussed. Leave it out by default. If you need a feature
99% of users do not need, it is entirely reasonable that you should need to add
a new dependency for it to start working.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[TLDR: if capabilities are kept in separate modules it mitigates the risk of a capability being compromised.]]></summary></entry><entry><title type="html">GitHub Actions Caching Strategy</title><link href="https://blog.lidalia.org.uk/2020/10/github-actions-caching-strategy.html" rel="alternate" type="text/html" title="GitHub Actions Caching Strategy" /><published>2020-10-13T21:48:35+00:00</published><updated>2020-10-13T21:48:35+00:00</updated><id>https://blog.lidalia.org.uk/2020/10/github-actions-caching-strategy</id><content type="html" xml:base="https://blog.lidalia.org.uk/2020/10/github-actions-caching-strategy.html"><![CDATA[<p>I’ve been trying to come up with a good caching strategy for a CI build, and
with the new <code class="language-plaintext highlighter-rouge">cache@v2</code> action I think I’ve worked it out.</p>

<p>I want:</p>
<ul>
  <li>a nightly build without cache, to prove that the build works from scratch
and isn’t being propped up by a cache that cannot be recreated</li>
  <li>a regularly renewed cache, so that it doesn’t keep growing - often the
cache retrieval, expansion &amp; update is the longest part of my build.</li>
</ul>

<p>It occurred to me that these two requirements play nicely together - if the
nightly build could start with no cache, but prime the cache, then all builds
during the day would benefit from a nice minimal cache.</p>

<p>I think the following GitHub action achieves this, by including the current
date in the base cache key:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">My Build</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
    <span class="c1"># Daily at 2AM</span>
    <span class="c1"># * is a special character in YAML so you have to quote this string</span>
    <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">0</span><span class="nv"> </span><span class="s">2</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*'</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">cache-name</span><span class="pi">:</span> <span class="s">my-build-1</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-18.04</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v2</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get current date</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">date</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">echo "::set-output name=date::$(date +'%Y-%m-%d')"</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cache whatever</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/cache@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">~/path_needing_caching</span>
          <span class="c1"># Always want a cache miss on the first build of the day, which should</span>
          <span class="c1"># be the scheduled 2AM one. Proves the build works from scratch, and</span>
          <span class="c1"># primes a nice clean cache to work with each day.</span>
          <span class="na">key</span><span class="pi">:</span> <span class="s">${{ env.cache-name }}_${{ steps.date.outputs.date }}-${{ github.ref }}-${{ github.run_number }}</span>
          <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
               <span class="s">${{ env.cache-name }}_${{ steps.date.outputs.date }}-${{ github.ref }}-</span>
               <span class="s">${{ env.cache-name }}_${{ steps.date.outputs.date }}-</span>
</code></pre></div></div>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[I’ve been trying to come up with a good caching strategy for a CI build, and with the new cache@v2 action I think I’ve worked it out.]]></summary></entry><entry><title type="html">Intersection Types in Kotlin</title><link href="https://blog.lidalia.org.uk/2020/05/kotlin-intersection-types.html" rel="alternate" type="text/html" title="Intersection Types in Kotlin" /><published>2020-05-14T11:30:00+00:00</published><updated>2020-05-14T11:30:00+00:00</updated><id>https://blog.lidalia.org.uk/2020/05/kotlin-intersection-types</id><content type="html" xml:base="https://blog.lidalia.org.uk/2020/05/kotlin-intersection-types.html"><![CDATA[<p>I recently found myself wanting intersection types in Kotlin. Specifically, I
was looking to write a client for WebDriver that depended on its <code class="language-plaintext highlighter-rouge">WebDriver</code> and
<code class="language-plaintext highlighter-rouge">HasInputDevices</code> interfaces but not on a concrete implementation like
<code class="language-plaintext highlighter-rouge">RemoteWebDriver</code>.</p>

<p>They are not natively supported yet (see <a href="https://youtrack.jetbrains.com/issue/KT-13108">KT-13108: Denotable union and intersection types</a>).
However, you can get something very like them using Kotlin’s <code class="language-plaintext highlighter-rouge">by</code> delegation:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1">// Third party code - I can't change</span>
<span class="kd">interface</span> <span class="nc">WebDriver</span>
<span class="kd">interface</span> <span class="nc">HasInputDevices</span>
<span class="kd">class</span> <span class="nc">RemoteWebDriver</span> <span class="p">:</span> <span class="nc">WebDriver</span><span class="p">,</span> <span class="nc">HasInputDevices</span>
<span class="kd">class</span> <span class="nc">EventFiringWebDriver</span> <span class="p">:</span> <span class="nc">WebDriver</span><span class="p">,</span> <span class="nc">HasInputDevices</span>

<span class="c1">// My code</span>
<span class="kd">interface</span> <span class="nc">CompositeWebDriver</span> <span class="p">:</span> <span class="nc">WebDriver</span><span class="p">,</span> <span class="nc">HasInputDevices</span>

<span class="kd">class</span> <span class="nc">CompositeRemoteWebDriver</span><span class="p">(</span>
  <span class="n">delegate</span><span class="p">:</span> <span class="nc">RemoteWebDriver</span> <span class="p">=</span> <span class="nc">RemoteWebDriver</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">CompositeWebDriver</span><span class="p">,</span>
  <span class="nc">WebDriver</span> <span class="k">by</span> <span class="n">delegate</span><span class="p">,</span> 
  <span class="nc">HasInputDevices</span> <span class="k">by</span> <span class="n">delegate</span>

<span class="kd">class</span> <span class="nc">CompositeEventFiringWebDriver</span><span class="p">(</span>
  <span class="n">delegate</span><span class="p">:</span> <span class="nc">EventFiringWebDriver</span> <span class="p">=</span> <span class="nc">EventFiringWebDriver</span><span class="p">()</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">CompositeWebDriver</span><span class="p">,</span>
  <span class="nc">WebDriver</span> <span class="k">by</span> <span class="n">delegate</span><span class="p">,</span> 
  <span class="nc">HasInputDevices</span> <span class="k">by</span> <span class="n">delegate</span>

<span class="kd">class</span> <span class="nc">WebDriverClient</span><span class="p">(</span><span class="n">driver</span><span class="p">:</span> <span class="nc">CompositeWebDriver</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// code depending on methods in WebDriver &amp; HasInputDevices</span>
<span class="p">}</span>

<span class="c1">// Client can now depend on either implementation</span>
<span class="kd">val</span> <span class="py">client1</span> <span class="p">=</span> <span class="nc">WebDriverClient</span><span class="p">(</span><span class="nc">CompositeRemoteWebDriver</span><span class="p">())</span>
<span class="kd">val</span> <span class="py">client2</span> <span class="p">=</span> <span class="nc">WebDriverClient</span><span class="p">(</span><span class="nc">CompositeEventFiringWebDriver</span><span class="p">())</span>
</code></pre></div></div>
<p>It’s still a bit painful; obviously there’s a reasonable amount of repetition in
the declaration of each implementation, and adding a new interface to
<code class="language-plaintext highlighter-rouge">CompositeWebDriver</code>’s supertypes means also adding it as an extra supertype to
every implementation with a <code class="language-plaintext highlighter-rouge">by delegate</code> declaration. Still, it decouples the
client code from the actual third party implementation without having to
implement every method in each interface.</p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[I recently found myself wanting intersection types in Kotlin. Specifically, I was looking to write a client for WebDriver that depended on its WebDriver and HasInputDevices interfaces but not on a concrete implementation like RemoteWebDriver.]]></summary></entry></feed>