<?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>2024-10-19T11:58:51+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><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><entry><title type="html">Tips for Using Gradle to build JPMS Modules when Developing with IntelliJ</title><link href="https://blog.lidalia.org.uk/2019/11/gradle-jpms-intellij-tips.html" rel="alternate" type="text/html" title="Tips for Using Gradle to build JPMS Modules when Developing with IntelliJ" /><published>2019-11-23T13:05:00+00:00</published><updated>2019-11-23T13:05:00+00:00</updated><id>https://blog.lidalia.org.uk/2019/11/gradle-jpms-intellij-tips</id><content type="html" xml:base="https://blog.lidalia.org.uk/2019/11/gradle-jpms-intellij-tips.html"><![CDATA[<p>I’ve recently been trying to get a project going with the following stack:</p>

<ul>
  <li>Language: <a href="https://kotlinlang.org/">Kotlin</a></li>
  <li>Target: <a href="https://openjdk.java.net/projects/jigsaw/spec/">JVM Module (JPMS)</a></li>
  <li>UI Framework: <a href="https://openjfx.io/">OpenJavaFX</a></li>
  <li>Build system: <a href="https://gradle.org/">Gradle</a> using the <a href="https://docs.gradle.org/current/userguide/kotlin_dsl.html">Kotlin DSL</a></li>
  <li>IDE: <a href="https://www.jetbrains.com/idea/">IntelliJ IDEA</a></li>
</ul>

<p>In principle this didn’t seem an unreasonable stack. JavaFX is meant to be the
latest and greatest JVM UI Framework. It requires JPMS usage, but then JPMS is
meant to be the new “correct” way to develop for the JVM. Gradle is the oldest
and most mature JVM build system that isn’t awful (no I don’t want to program in
XML). It has good IntelliJ support. Kotlin is developed by IntelliJ and uses
gradle as its build system; I’ve been programming with it for a year or two now
and found it mature, well designed and a pleasure to use.</p>

<p>In practice I found it surprisingly painful to get things to a) work and b) work
the way I wanted them to. So if you’re doing something similar you can now learn
from my pain.</p>

<p>Tips:</p>

<h2 id="add-plugins-as-runtime-dependencies-in-buildsrc">Add plugins as runtime dependencies in <code class="language-plaintext highlighter-rouge">buildSrc/</code></h2>

<h3 id="buildsrcbuildgradlekts"><code class="language-plaintext highlighter-rouge">buildSrc/build.gradle.kts</code></h3>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">plugins</span> <span class="p">{</span>
  <span class="n">`kotlin-dsl`</span>
<span class="p">}</span>

<span class="nf">repositories</span> <span class="p">{</span>
  <span class="nf">gradlePluginPortal</span><span class="p">()</span>
<span class="p">}</span>

<span class="nf">dependencies</span> <span class="p">{</span>
  <span class="nf">testRuntimeOnly</span><span class="p">(</span><span class="s">"org.javamodularity:moduleplugin:1.5.0"</span><span class="p">)</span>
  <span class="nf">testRuntimeOnly</span><span class="p">(</span><span class="s">"org.openjfx:javafx-plugin:0.0.8"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This was the most useful thing for me in debugging plugin behaviour. Having a
<code class="language-plaintext highlighter-rouge">buildSrc/build.gradle.kts</code> with the <code class="language-plaintext highlighter-rouge">kotlin-dsl</code> plugin makes IntelliJ add the
gradle jars &amp; sources to <code class="language-plaintext highlighter-rouge">External Libraries</code>. Adding any plugins you use as
runtime dependencies in <code class="language-plaintext highlighter-rouge">buildSrc/</code> means they also get added to
<code class="language-plaintext highlighter-rouge">External Libraries</code>. This allows you to browse their source, hyperlink around
it and set breakpoints. I find it invaluable in debugging plugin behaviour.</p>

<p>(This tip may become irrelevant if a future version of IntelliJ automatically
adds these sources to  <code class="language-plaintext highlighter-rouge">External Libraries</code> - vote on
<a href="https://youtrack.jetbrains.com/issue/IDEA-197182">IDEA-197182</a> to help make
that happen.)</p>

<h2 id="plugin-application-order-matters">Plugin application order matters</h2>

<p>I am using the <a href="https://github.com/java9-modularity/gradle-modules-plugin">org.javamodularity:moduleplugin</a>
to manage the JPMS behaviour because gradle has no out of the box support. It
needs to read the java main sourceSet to find &amp; read <code class="language-plaintext highlighter-rouge">module-info.java</code>. If you
change the sourceSet directories it will pick up on that change - but you need
to apply moduleplugin <em>after</em> you change the srcSets, not before. Otherwise it
picks up the standard src dirs and cannot find the file.</p>

<p>Irritatingly when it doesn’t find <code class="language-plaintext highlighter-rouge">module-info.java</code> it just silently does not
apply itself, leaving you to try and understand the downstream errors, rather
than failing good and hard and explaining the problem to you.</p>

<h2 id="use-moduleplugin-version-150-not-160">Use moduleplugin version 1.5.0 not 1.6.0</h2>

<p>I encountered two issues with version 1.6.0:</p>
<ul>
  <li>It has a breaking change (on a minor version increment…) and
<a href="https://github.com/openjfx/javafx-gradle-plugin">javafx-plugin:0.0.8</a> depends
on a class that is no longer present in 1.6.0</li>
  <li>It gets the module path wrong when trying to run a standard mixed (java &amp;
kotlin) module</li>
</ul>

<h2 id="things-you-just-need-to-know-about-the-gradle-kotlin-dsl">Things you just need to know about the gradle kotlin dsl</h2>

<p>Configuring sub project plugins in a root project that should not apply the
plugins is hard and confusing. Trial and error have shown me the following:</p>

<p>Import configurations:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">api</span> <span class="k">by</span> <span class="n">configurations</span>
<span class="kd">val</span> <span class="py">implementation</span> <span class="k">by</span> <span class="n">configurations</span>
<span class="kd">val</span> <span class="py">testImplementation</span> <span class="k">by</span> <span class="n">configurations</span>

<span class="nf">dependencies</span> <span class="p">{</span>
  <span class="nf">api</span><span class="p">(</span><span class="nf">kotlin</span><span class="p">(</span><span class="s">"stdlib"</span><span class="p">))</span>
  <span class="nf">implementation</span><span class="p">(</span><span class="s">"..."</span><span class="p">)</span>
  <span class="nf">testImplementation</span><span class="p">(</span><span class="s">"..."</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Import tasks:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">test</span> <span class="k">by</span> <span class="n">tasks</span><span class="p">.</span><span class="nf">existing</span><span class="p">(</span><span class="nc">Test</span><span class="o">::</span><span class="k">class</span><span class="p">)</span>
<span class="nf">tasks</span> <span class="p">{</span>
  <span class="nf">test</span> <span class="p">{</span>
    <span class="nf">useJUnitPlatform</span><span class="p">()</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Configure java:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">configure</span><span class="p">&lt;</span><span class="nc">JavaPluginExtension</span><span class="p">&gt;</span> <span class="p">{</span>
  <span class="n">sourceCompatibility</span> <span class="p">=</span> <span class="n">javaVersion</span>
  <span class="n">targetCompatibility</span> <span class="p">=</span> <span class="n">javaVersion</span>

  <span class="n">configure</span><span class="p">&lt;</span><span class="nc">SourceSetContainer</span><span class="p">&gt;</span> <span class="p">{</span>
    <span class="nf">named</span><span class="p">(</span><span class="s">"main"</span><span class="p">)</span> <span class="p">{</span> <span class="n">java</span><span class="p">.</span><span class="nf">setSrcDirs</span><span class="p">(</span><span class="nf">setOf</span><span class="p">(</span><span class="s">"src"</span><span class="p">))</span> <span class="p">}</span>
    <span class="nf">named</span><span class="p">(</span><span class="s">"test"</span><span class="p">)</span> <span class="p">{</span> <span class="n">java</span><span class="p">.</span><span class="nf">setSrcDirs</span><span class="p">(</span><span class="nf">setOf</span><span class="p">(</span><span class="s">"tests"</span><span class="p">))</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Configure kotlin:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">configure</span><span class="p">&lt;</span><span class="nc">KotlinJvmProjectExtension</span><span class="p">&gt;</span> <span class="p">{</span>
  <span class="nf">sourceSets</span> <span class="p">{</span>
    <span class="nf">named</span><span class="p">(</span><span class="s">"main"</span><span class="p">)</span> <span class="p">{</span> <span class="n">kotlin</span><span class="p">.</span><span class="nf">setSrcDirs</span><span class="p">(</span><span class="nf">setOf</span><span class="p">(</span><span class="s">"src"</span><span class="p">))</span> <span class="p">}</span>
    <span class="nf">named</span><span class="p">(</span><span class="s">"test"</span><span class="p">)</span> <span class="p">{</span> <span class="n">kotlin</span><span class="p">.</span><span class="nf">setSrcDirs</span><span class="p">(</span><span class="nf">setOf</span><span class="p">(</span><span class="s">"tests"</span><span class="p">))</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="consider-applying-config-by-applied-plugin">Consider applying config by applied plugin</h2>

<p>If you’ve got a multi-project build it may be convenient to configure sub
projects by whether or not they have some plugin applied to them. In my root
project I do this:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">subprojects</span> <span class="p">{</span>
  <span class="n">pluginManager</span><span class="p">.</span><span class="nf">withPlugin</span><span class="p">(</span><span class="s">"kotlin"</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// All config common to kotlin projects</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then in any sub project I just need this in <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> to apply all my
common kotlin config:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">plugins</span> <span class="p">{</span>
  <span class="nf">kotlin</span><span class="p">(</span><span class="s">"jvm"</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This has the additional benefit that, because you explicitly applied the plugin,
you have access to the plugin’s dsl in that sub project’s <code class="language-plaintext highlighter-rouge">build.gradle.kts</code>.</p>

<h2 id="share-functions-between-multiple-buildgradlekts-by-putting-them-in-buildsrc">Share functions between multiple <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> by putting them in <code class="language-plaintext highlighter-rouge">buildSrc/</code></h2>

<p>Any code in <code class="language-plaintext highlighter-rouge">buildSrc/&lt;main src dir&gt;</code> is available to all <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> in
the project and sub projects.</p>

<p>For instance if I create a file in <code class="language-plaintext highlighter-rouge">buildSrc/&lt;main src dir&gt;</code> called
<code class="language-plaintext highlighter-rouge">DependencyVersions.kt</code> like so:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">fun</span> <span class="nf">kotlintest</span><span class="p">(</span><span class="n">module</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">=</span> <span class="s">"io.kotlintest:kotlintest-$module:3.4.2"</span>
<span class="k">fun</span> <span class="nf">arrowkt</span><span class="p">(</span><span class="n">module</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">=</span> <span class="s">"io.arrow-kt:arrow-$arrowModule:0.10.2"</span>
<span class="k">fun</span> <span class="nf">kotlinCoroutines</span><span class="p">(</span><span class="n">module</span><span class="p">:</span> <span class="nc">String</span><span class="p">)</span> <span class="p">=</span> <span class="s">"org.jetbrains.kotlinx:kotlinx-coroutines-$module:1.3.2"</span>
</code></pre></div></div>

<p>then in any <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> I can use those functions to depend on modules as
so:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">dependencies</span> <span class="p">{</span>
  <span class="nf">implementation</span><span class="p">(</span><span class="nf">kotlintest</span><span class="p">(</span><span class="s">"core"</span><span class="p">))</span>
  <span class="nf">implementation</span><span class="p">(</span><span class="nf">arrowkt</span><span class="p">(</span><span class="s">"core"</span><span class="p">))</span>
  <span class="nf">implementation</span><span class="p">(</span><span class="nf">kotlinCoroutines</span><span class="p">(</span><span class="s">"core"</span><span class="p">))</span>
  <span class="nf">implementation</span><span class="p">(</span><span class="nf">kotlinCoroutines</span><span class="p">(</span><span class="s">"javafx"</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="prevent-intermediate-directories-becoming-projects">Prevent intermediate directories becoming projects</h2>

<p>By default, if you include deeply nested projects like this:</p>

<h4 id="settingsgradlekts"><code class="language-plaintext highlighter-rouge">settings.gradle.kts</code></h4>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">include</span><span class="p">(</span>
  <span class="s">":app"</span><span class="p">,</span>
  <span class="s">":core"</span><span class="p">,</span>
  <span class="s">":ui:api"</span><span class="p">,</span>
  <span class="s">":ui:javafx"</span>
<span class="p">)</span>
</code></pre></div></div>
<p>the intermediate directories (in this case <code class="language-plaintext highlighter-rouge">ui</code>) will become gradle projects,
despite lacking a <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> and any other files. This can cause very
confusing errors like this one:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':app'.
&gt; A problem occurred configuring project ':ui:api'.
   &gt; Could not open cache directory add8lpbh91wftlbit7lhn37cw (/home/runner/.gradle/caches/6.0.1/gradle-kotlin-dsl/add8lpbh91wftlbit7lhn37cw).
      &gt; org.gradle.api.internal.initialization.DefaultClassLoaderScope@47f3a892 must be locked before it can be used to compute a classpath!
</code></pre></div></div>

<p>You can fix this by including the specific project and setting its dir
explicitly, as so:</p>

<h4 id="settingsgradlekts-1"><code class="language-plaintext highlighter-rouge">settings.gradle.kts</code></h4>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">include</span><span class="p">(</span>
  <span class="s">":app"</span><span class="p">,</span>
  <span class="s">":core"</span><span class="p">,</span>
  <span class="s">":ui-api"</span><span class="p">,</span>
  <span class="s">":ui-javafx"</span>
<span class="p">)</span>
<span class="nf">project</span><span class="p">(</span><span class="s">":ui-api"</span><span class="p">).</span><span class="n">projectDir</span> <span class="p">=</span> <span class="nf">file</span><span class="p">(</span><span class="s">"ui/api"</span><span class="p">)</span>
<span class="nf">project</span><span class="p">(</span><span class="s">":ui-javafx"</span><span class="p">).</span><span class="n">projectDir</span> <span class="p">=</span> <span class="nf">file</span><span class="p">(</span><span class="s">"ui/javafx"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="example-project">Example Project</h2>

<p>These ideas can be seen implemented at
<a href="https://github.com/Mahoney-example/example-gradle-kotlin-javafx">https://github.com/Mahoney-example/example-gradle-kotlin-javafx</a></p>]]></content><author><name>Robert Elliot</name></author><summary type="html"><![CDATA[I’ve recently been trying to get a project going with the following stack:]]></summary></entry></feed>