<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://coderanger.net/</id>
  <title>Noah Kantrowitz</title>
  <updated>2017-12-05T05:00:00Z</updated>
  <link rel="alternate" href="https://coderanger.net/"/>
  <link rel="self" href="https://coderanger.net/atom.xml"/>
  <author>
    <name>Noah Kantrowitz</name>
    <uri>https://coderanger.net/</uri>
  </author>
  <icon>https://coderanger.net/favicon.png</icon>
  <entry>
    <id>tag:coderanger.net,2017-12-05:/jenkins/</id>
    <title type="html">Jenkins Wrangling for Fun &amp;amp; Profit</title>
    <published>2017-12-05T05:00:00Z</published>
    <updated>2017-12-06T00:00:34Z</updated>
    <link rel="alternate" href="https://coderanger.net/jenkins/"/>
    <content type="html">&lt;p&gt;While there have been &lt;a href="https://travis-ci.org/"&gt;many&lt;/a&gt; &lt;a href="https://about.gitlab.com/features/gitlab-ci-cd/"&gt;new&lt;/a&gt;
&lt;a href="https://www.gocd.org/"&gt;developments&lt;/a&gt; in CI/testing tools, &lt;a href="https://jenkins.io/"&gt;Jenkins&lt;/a&gt;
is still a mainstay. And to be fair to the Jenkins team, it has come a tremendous
way in the past few years. The new Pipelines system is more flexible than anything
I’ve used before, and the Blue Ocean UI is a big graphical and UX upgrade.
My team at SAP started using Jenkins long before I arrived, but over the
years we have slowly accumulated some complaints about how it was working and how
we managed things.&lt;/p&gt;

&lt;p&gt;This post is going to be a (very) long-form dive into how we set things up and why.
I do not think this is going to work out of the box for (almost) anyone else, but
the hope is that this will provide a blueprint for others to build their own
solutions on the same general ideas.&lt;/p&gt;

&lt;h3 id="the-problems"&gt;The Problems&lt;/h3&gt;

&lt;p&gt;Before I launch into what we did, let’s list out the issues we had with our
current set up so we are all on the same page. Leaving aside some details that
exist only for PCI, we have a Jenkins server deployed by Chef. Some plugins were
installed originally by the Chef cookbook, but most were installed and upgraded
by hand since then. Jobs were mostly created via a custom CLI tool that talks
to the Jenkins API, but then updated by hand (or more often, not) after that.&lt;/p&gt;

&lt;p&gt;So we spent some time in a meeting room with a whiteboard and came up with a
few top-level problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upgrades are too unpredictable, both for Jenkins and individual plugins.&lt;/li&gt;
  &lt;li&gt;Jenkins configuration is (mostly) not versioned, ditto for job configs.&lt;/li&gt;
  &lt;li&gt;Job configs can bitrot over time with no easy way to update them other than
one at a time.&lt;/li&gt;
  &lt;li&gt;No pull-request builds, and overall existing builds are quite slow.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;While this applied to a lot of different use cases for Jenkins, the one I chose
to tackle first was Chef cookbook testing, but with a clear eye towards building
a solution which will grow to include other use cases as we want to move them
off the old Jenkins server.&lt;/p&gt;

&lt;h3 id="the-shape"&gt;The Shape&lt;/h3&gt;

&lt;p&gt;After a bunch of research, the overall shape of the goal came together fairly
quickly. We’ll go through each of these in excruciating detail later on, but to
start let’s break it down into bullet points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deployed on Kubernetes, because this is the way we’re trying to move everything.&lt;/li&gt;
  &lt;li&gt;Build a container image containing Jenkins, all the plugins, and configuration.&lt;/li&gt;
  &lt;li&gt;Use Helm for managing the deployment (and rollback if needed).&lt;/li&gt;
  &lt;li&gt;Manage the configuration via Jenkins groovy as much as possible.&lt;/li&gt;
  &lt;li&gt;Use the “organization folder” system in Jenkins to auto-detect projects.&lt;/li&gt;
  &lt;li&gt;Use shared pipeline libraries to keep the per-repository config low.&lt;/li&gt;
  &lt;li&gt;Build a container image for the cookbook testing environment that has all
needed gems pre-installed.&lt;/li&gt;
  &lt;li&gt;Work out a way to test cookbooks on top of Kubernetes pods.&lt;/li&gt;
&lt;/ol&gt;&lt;h3 id="kubernetes"&gt;Kubernetes?&lt;/h3&gt;

&lt;p&gt;We did briefly look over non-Kubernetes deployment options like building a new Chef
cookbook or using a dedicated Nomad cluster, but with the continued rise of
Kubernetes as an operations platform it seemed like a good idea to use this as
an internal experiment for running “real” (but not customer facing) services
on Kubernetes. In the months since that choice, I think we have only seen the
industry move even more behind Kubernetes as the next dominant platform so this
seems to have been the right move. If you already have a heavy investment in
Mesos or Nomad then perhaps just ignore the Kubernetes-specific bits of this.&lt;/p&gt;

&lt;p&gt;Within the Kubernetes ecosystem there are a few tools/patterns for managing
deployment of complex applications (i.e. things that need more than just a pod).
While &lt;a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#apply"&gt;“folder full of YAML + kubectl”&lt;/a&gt;
and &lt;a href="http://ksonnet.heptio.com/"&gt;ksonnet&lt;/a&gt; are nice from a simplicity point-of-view,
the rollback capabilities of &lt;a href="https://helm.sh/"&gt;Helm&lt;/a&gt; made it the clear choice in my mind.&lt;/p&gt;

&lt;p&gt;The deployment of Kubernetes and Helm themselves are out-of-scope for this post;
there are numerous guides for Kubernetes and setting up Helm is mostly just &lt;code&gt;helm init&lt;/code&gt;.
Our production cluster is currently on AWS and set up with Kops, but your mileage
may vary if you aren’t on AWS. If you just want to play with the stuff in this
post, I would highly recommend starting with a hosted cluster option like Google
GKE, Azure AKS, or the newly announced Amazon EKS.&lt;/p&gt;

&lt;h3 id="the-jenkins-container"&gt;The Jenkins Container&lt;/h3&gt;

&lt;p&gt;The first stop on our journey is building a Docker image for the Jenkins server.
Going line by line so we can talk about it as we go:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;FROM jenkins/jenkins:2.92-alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We’re starting from the existing Jenkins Docker Hub images. This determines
which version of Jenkins gets used so doing a Jenkins upgrade consists of changing
this line, building a new container, pushing it to our registry, and then upgrading
the Helm release. This is using the Jenkins weekly release, so we try to keep
this bumped roughly once a week, though if it ends up a few weeks behind that’s
totally fine.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;COPY saml-idp-metadata.xml /metadata.xml
COPY plugins.txt /plugins.txt
COPY style.css /style.css
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next we copy some base files. The SAP internal authentication system uses SAML
(please hold your sighs for the end) so we store the IdP metadata in a file to
use in the Jenkins config. The plugins.txt looks like:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;kubernetes:1.1
workflow-aggregator:2.5
workflow-job:2.15
credentials-binding:1.13
git:3.6.4
blueocean:1.3.3
github-oauth:0.28.1
matrix-auth:2.2
saml:1.0.4
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and gets used later down by the &lt;code&gt;install-plugins.sh&lt;/code&gt; script that comes in the
base image. The &lt;code&gt;style.css&lt;/code&gt; file is a few minor tweaks on top of the theme for
things it gets wrong or we didn’t like:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-css"&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nt"&gt;The&lt;/span&gt; &lt;span class="nt"&gt;theme&lt;/span&gt; &lt;span class="nt"&gt;overwrites&lt;/span&gt; &lt;span class="nt"&gt;this&lt;/span&gt; &lt;span class="nt"&gt;so&lt;/span&gt; &lt;span class="nt"&gt;we&lt;/span&gt; &lt;span class="nt"&gt;need&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="nt"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="nc"&gt;.glyphicon&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;font-family&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Glyphicons Halflings'&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nt"&gt;Theme&lt;/span&gt; &lt;span class="nt"&gt;has&lt;/span&gt; &lt;span class="nt"&gt;no&lt;/span&gt; &lt;span class="nt"&gt;icon&lt;/span&gt; &lt;span class="nt"&gt;for&lt;/span&gt; &lt;span class="nt"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="nc"&gt;.icon-github-branch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;background-image&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/static/ccf6b398/plugin/github-branch-source/images/24x24/github-branch.png')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="nt"&gt;Force&lt;/span&gt; &lt;span class="nt"&gt;all&lt;/span&gt; &lt;span class="nt"&gt;icons&lt;/span&gt; &lt;span class="nt"&gt;in&lt;/span&gt; &lt;span class="nt"&gt;that&lt;/span&gt; &lt;span class="nt"&gt;bar&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="nt"&gt;be&lt;/span&gt; &lt;span class="nt"&gt;grayscale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="nc"&gt;.icon-md&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grayscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Overall files that aren’t expected to change often.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;RUN mkdir -p /usr/share/jenkins/ref/secrets &amp;amp;&amp;amp; \
    # Why is this not the default?
    echo false &amp;gt; /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch &amp;amp;&amp;amp; \
    # Install all our plugins so they are baked in to the image.
    /usr/local/bin/install-plugins.sh &amp;lt; /plugins.txt &amp;amp;&amp;amp; \
    # Install a nicer default theme to make it look shiny for non-BlueOcean.
    mkdir /usr/share/jenkins/ref/userContent &amp;amp;&amp;amp; \
    curl --compressed http://jenkins-contrib-themes.github.io/jenkins-neo-theme/dist/neo-light.css &amp;gt; /usr/share/jenkins/ref/userContent/neo-light.css.override &amp;amp;&amp;amp; \
    cat /style.css &amp;gt;&amp;gt; /usr/share/jenkins/ref/userContent/neo-light.css.override
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the meat of the Dockerfile. A poorly documented feature of the Jenkins
Docker image is that all files under &lt;code&gt;/usr/share/jenkins/ref&lt;/code&gt; are used to seed
the creation of the JENKINS_HOME folder during startup. Normally these files are
only copied over the first time, but if they end in &lt;code&gt;.override&lt;/code&gt; it is copied every
time Jenkins starts (with the &lt;code&gt;.override&lt;/code&gt; trimmed off).&lt;/p&gt;

&lt;p&gt;First we set the poorly named &lt;code&gt;slave-to-master-security-kill-switch&lt;/code&gt; file which
makes it so JNLP builders don’t get admin access to the Jenkins server because
we don’t want rogue builds to take down the universe if possible.&lt;/p&gt;

&lt;p&gt;Next we install all the Jenkins plugins. It should be noted that the Docker
layer cache can sometimes bite you here. Because we only list the top-level
plugins we want (the script handles finding all dependencies), if we want to
upgrade an internal dependency but nothing else has changed, might need to
manually zap the cached layer image. Given that Jenkins itself releases weekly
anyway (meaning we change the &lt;code&gt;FROM&lt;/code&gt; image and invalidate the whole cache), it’s
not hugely likely that this will an operational problem, but be aware.&lt;/p&gt;

&lt;p&gt;After that we set up some custom CSS theming. While Blue Ocean does have a nicely
refreshed UI, the default post-login landing page is still the normal UI and we
wanted to spruce that up a bit, both for aesthetic reasons and to make it easier
to tell at a glance which Jenkins server you are looking at. Neo-light seemed
the nicest of the themes that still worked, but you can change or ignore this
part as you wish.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;COPY config.groovy /usr/share/jenkins/ref/init.groovy.d/zzz_alti-jenkins.groovy.override
COPY plugin/target/alti-jenkins-plugin.hpi /usr/share/jenkins/ref/plugins/alti-jenkins-plugin.hpi.override
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And finally we copy over two more files. These change more often than the plugins.txt
or Jenkins version (at least so far during development, hopefully that will change
over time) so they go at the end. The config Groovy code gets put in place to be
run automatically at startup, with the weird &lt;code&gt;zzz&lt;/code&gt; thing because Jenkins alpha-sorts
the hook scripts if there is more than one and we want to be last. The config
itself is big and complex so we’ll cover that further down.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;alti-jenkins&lt;/code&gt; plugin is a bit of an experiment, currently all it does is
add the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags to the HTML for the theme CSS and sets a few security HTTP
headers. This could probably be replaced with the &lt;code&gt;simple-theme&lt;/code&gt; plugin instead,
but I would like to add more stuff to it (ex. custom job health metric that
ignores failed PR builds), so we’re leaving it for now.&lt;/p&gt;

&lt;p&gt;And with that, we have a Jenkins Docker image. &lt;code&gt;docker build -t ourrepo.com/alti_jenkins:2.92 .&lt;/code&gt;,
&lt;code&gt;docker push ourrepo.com/alti_jenkins:2.92&lt;/code&gt;, ???, profit. As a built artifact this
encompasses the Jenkins release, all plugins used, and the configuration code.
Just about everything we could ask for.&lt;/p&gt;

&lt;p&gt;We’ll talk about the &lt;code&gt;config.groovy&lt;/code&gt; in just a moment, but because all secrets
(or any other configuration you want to hide) is coming in at run-time from
Kubernetes, this image doesn’t actually contain anything that needs to be hidden.
If you aren’t running your own registry already, you could push this up to a
public Docker Hub account instead.&lt;/p&gt;

&lt;h3 id="section"&gt;&lt;code&gt;config.groovy&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;This was the bulk of my time on the project, a slowly expanding config script
that started with some basics and now encompasses the entire setup process.
I will lead off with the fact that I am neither a Jenkins nor Groovy expert so
I’m sure this code can be improved, for example I only learned very late in my
writing that &lt;code&gt;import&lt;/code&gt; is optional in Groovy if you use the fully-qualified
class name. With that in mind, let’s go line by line again:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;static&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The most important import, the Jenkins object singleton. We use this a ton, so
put it in a magic global.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.cloudbees.plugins.credentials.*&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.cloudbees.plugins.credentials.common.*&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.cloudbees.plugins.credentials.domains.*&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.cloudbees.plugins.credentials.impl.*&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hudson.security.GlobalMatrixAuthorizationStrategy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hudson.security.Permission&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;hudson.util.Secret&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jenkins.branch.OrganizationFolder&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jenkins.install.InstallState&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jenkins.plugins.git.GitSCMSource&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.csanchez.jenkins.plugins.kubernetes.PodTemplate&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.csanchez.jenkins.plugins.kubernetes.ServiceAccountCredential&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.github_branch_source.BranchDiscoveryTrait&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.github_branch_source.GitHubSCMNavigator&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.github_branch_source.OriginPullRequestDiscoveryTrait&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.saml.SamlEncryptionData&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.saml.SamlSecurityRealm&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.workflow.libs.LibraryConfiguration&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.jenkinsci.plugins.workflow.libs.SCMSourceRetriever&lt;/span&gt;

&lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; configuring alti_jenkins"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As mentioned, I didn’t really know how Groovy imports worked when starting this
so this is mostly not needed but I haven’t cleaned it up yet.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default, if an init hook script fails, Jenkins prints an error to the log and
keeps on truckin’. The whole config code is inside a &lt;code&gt;try/catch&lt;/code&gt; so we can at
least attempt to not let it continue starting if we might have failed to configure
something important (like, say, authentication). This will only catch runtime
errors though, if there is a syntax error in the script, that will still result
in Jenkins starting as per usual.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// CONFIG&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;secretsRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'JENKINS_SECRETS'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'/var/jenkins_secrets'&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;downwardRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DOWNWARD_VOLUME'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'/etc/downward'&lt;/span&gt;
  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; Loading configuration from from secrets:$secretsRoot and downward:$downwardRoot"&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;githubUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$secretsRoot/github-user"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;githubUserToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$secretsRoot/github-token"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;samlPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$secretsRoot/saml-pass"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;samlKeystore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$secretsRoot/saml-keystore"&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;developmentMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$secretsRoot/development-mode"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;kubeNamespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$downwardRoot/namespace"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;admins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'nkantrowitz'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Noah Kantrowitz&lt;/span&gt;
    &lt;span class="s1"&gt;'etc'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Someone else&lt;/span&gt;
  &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;githubOrg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MyOrg'&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;librariesRepo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$githubOrg/jenkins-pipeline-libs"&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;agentVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'3.10-1-alpine'&lt;/span&gt;

  &lt;span class="c1"&gt;// Parse the labels test.&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[:]&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$downwardRoot/labels"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;eachLine&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next up, loading and parsing a bunch of configuration data. We’ll look at the
pod configuration later on, but this is mostly reading from either a Kubernetes
Secret volume (for secrets) or a Downward API volume (for metadata about the pod
we are running inside of). And then a few hardcoded values that don’t change
often enough to be exposed outside of the file/image like the name of the GitHub
organization.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// GENERAL SETTINGS&lt;/span&gt;
  &lt;span class="c1"&gt;// Bypass the setup wizard because this script defines all of our config.&lt;/span&gt;
  &lt;span class="c1"&gt;// This is _supposed_ to be handled by /usr/share/jenkins/ref/jenkins.install.UpgradeWizard.state&lt;/span&gt;
  &lt;span class="c1"&gt;// but that doesn't seem to be working. See https://github.com/jenkinsci/docker#script-usage.&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSetupComplete&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Neutering SetupWizard'&lt;/span&gt;
    &lt;span class="n"&gt;InstallState&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INITIAL_SETUP_COMPLETED&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initializeState&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Disable CLI over the remoting protocol for security.&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDescriptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jenkins.CLI"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;// More security, disable old/unsafe agent protocols.&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentProtocols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"JNLP4-connect"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Ping"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Set&lt;/span&gt;
  &lt;span class="c1"&gt;// Enable CSRF.&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;crumbIssuer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;hudson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DefaultCrumbIssuer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Disable execution on the main server.&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;numExecutors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then some baseline global configuration. This disables the “welcome to Jenkins” setup
wizard, sets some security stuffs, and turns off job execution on the Jenkins
server itself because we want all jobs to run inside Kubernetes workers.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// AUTHENTICATION&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samlKeystore&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Configure the SAML plugin.&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Configuring SAML authentication realm'&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;realm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SamlSecurityRealm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/metadata.xml'&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String idpMetadata,&lt;/span&gt;
      &lt;span class="s1"&gt;'display_name'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String displayNameAttributeName,&lt;/span&gt;
      &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String groupsAttributeName,&lt;/span&gt;
      &lt;span class="n"&gt;SamlSecurityRealm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFAULT_MAXIMUM_AUTHENTICATION_LIFETIME&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Integer maximumAuthenticationLifetime,&lt;/span&gt;
      &lt;span class="s1"&gt;'uid'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String usernameAttributeName,&lt;/span&gt;
      &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;//  String emailAttributeName,&lt;/span&gt;
      &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String logoutUrl,&lt;/span&gt;
      &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SamlAdvancedConfiguration advancedConfiguration,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SamlEncryptionData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;// SamlEncryptionData encryptionData,&lt;/span&gt;
        &lt;span class="n"&gt;samlKeystore&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// String keystorePath,&lt;/span&gt;
        &lt;span class="n"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samlPass&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Secret keystorePassword,&lt;/span&gt;
        &lt;span class="n"&gt;Secret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samlPass&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Secret privateKeyPassword,&lt;/span&gt;
        &lt;span class="s1"&gt;'saml-key'&lt;/span&gt; &lt;span class="c1"&gt;// String privateKeyAlias&lt;/span&gt;
      &lt;span class="o"&gt;),&lt;/span&gt;
      &lt;span class="s1"&gt;'lowercase'&lt;/span&gt; &lt;span class="c1"&gt;// String usernameCaseConversion,&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;securityRealm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realm&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Not configuring SAML'&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO This shoud set up some fallback realm.&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ahh the fabled dev &lt;code&gt;TODO&lt;/code&gt;, I swear I’ll get back to that someday. This section is
setting up the authentication system for logging in to Jenkins, the “security realm”
in official parlance. For our production servers we’re using our company-wide
SAML SSO system because if I can ever not have to store passwords, I’ll take that
option in a heartbeat. If you don’t have a a similar internal SSO system, I
would recommend looking at the GitHub OAuth plugin, but you can always use the
internal login form realm if needed. If you are using SAML, the attribute
configuration options are likely to be different for you, but the rest should
look pretty similar. For unknown reasons, the Jenkins SAML plugin refers to the
SP signing key as “encryption data”, but it definitely is the signing key. You
can also see here is where we read back in the IdP metadata we embedded in the
Jenkins container image up above.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// AUTHORIZATION&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;developmentMode&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Turn off authorization in case hacking on SAML configs leads to lockout.&lt;/span&gt;
    &lt;span class="c1"&gt;// As it says in the values.yaml, do not do this in production.&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; Configuring Unsecured authorization strategy. THIS BETTER NOT BE PROD."&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;unsecured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;hudson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AuthorizationStrategy&lt;/span&gt;&lt;span class="n"&gt;$Unsecured&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizationStrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unsecured&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Configure matrix auth and ACLs.&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; Configuring Matrix authorization strategy."&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;authz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GlobalMatrixAuthorizationStrategy&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Hudson.Read"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Item.Build"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Item.Cancel"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Item.Discover"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Item.Read"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Item.Workspace"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Run.Replay"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"hudson.model.Run.Update"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Use the string form because I'm lazy and don't want to import all the things.&lt;/span&gt;
      &lt;span class="n"&gt;authz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;":authenticated"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Admins always get all permissions. Hopefully I won't regret this.&lt;/span&gt;
    &lt;span class="n"&gt;Permission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAll&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;perm&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;admins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;authz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;perm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizationStrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authz&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;While the authentication configuration (or security realm if you want to call it
that) determines how users log in, the authorization strategy decides what they
can do once logged in. For general use, we’re using the relatively-standard
Matrix authorizer from the plugin of the same name. If you need more complex
access controls you might want to look at the similarly more complex &lt;code&gt;role-strategy&lt;/code&gt;
authorizer. But here we only really have three bits of authorization config,
first is the &lt;code&gt;developmentMode&lt;/code&gt; setting coming in from Secret volume. If that is
set we entirely disable authorization. This in here for times where I need to
hack on the authentication config or if I’m offline somewhere and don’t have
access to the corporate SAML servers. Otherwise for the Matrix we set up one
entry for the generic “logged-in user” group to give them some minimal, read-only
permissions, which is enough for normal users to view builds and force them to
re-run if they end up with a flaky test (though hopefully they will fix it soon
after). For admins, we create one row for each administrator giving them every
permission available in Jenkins. Because this is rebuilt from scratch every time
the configuration script runs, it means that is we do manage to break the
permissions settings via “accidental” clicking in the web configuration GUI, it
will at least get automatically restored as soon as we restart the container.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// GITHUB CONFIG&lt;/span&gt;
  &lt;span class="c1"&gt;// Create the credentials used to access GitHub.&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CredentialsProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookupCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StandardUsernamePasswordCredentials&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;cred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findResult&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"GitHub access token"&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; Updating existing GitHub access token credential ${cred.id}"&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;newCred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UsernamePasswordCredentialsImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;githubUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;githubUserToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;SystemCredentialsProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newCred&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Creating GitHub access token credential'&lt;/span&gt;
    &lt;span class="n"&gt;cred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UsernamePasswordCredentialsImpl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;CredentialsScope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GLOBAL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
      &lt;span class="s2"&gt;"GitHub access token"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;githubUser&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;githubUserToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;SystemCredentialsProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next we construct a Jenkins credential with our GitHub access token. This was
read in up at the top from the Secret volume, and here we either create a new
credential if one isn’t found, or update the existing one. Again, the goal is
convergent behavior so every time Jenkins start, it tries to match the persistent
state to the desired state.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// GLOBAL LIBRARIES&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SCMSourceRetriever&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GitSCMSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"pipeline"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"https://github.com/${librariesRepo}.git/"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LibraryConfiguration&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pipeline"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"master"&lt;/span&gt;
  &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;implicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;includeInChangesets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDescriptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"org.jenkinsci.plugins.workflow.libs.GlobalLibraries"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setLibraries&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This configures the shared pipeline libraries. We’ll show the library code further
down, but roughly this allows having a centralized repo with Groovy snippets
that can be used by the per-repo Jenkinsfiles. In practical terms, this actually
contains almost all of the pipeline logic, the final Jenkinsfile is currently
always exactly one line long, calling one of the global presets. The &lt;code&gt;implicit&lt;/code&gt;
setting means Jenkinsfiles don’t have to explicitly include the library, and
disabling &lt;code&gt;includeInChangesets&lt;/code&gt; means that a new version of the library won’t
trigger every job to build (though that would certainly be a nice load test).&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// CLOUUUUUD (NOT BUTT)&lt;/span&gt;
  &lt;span class="c1"&gt;// Register the Kubernetes magic secret.&lt;/span&gt;
  &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CredentialsProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookupCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServiceAccountCredential&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Creating Kubernetes service account credential'&lt;/span&gt;
    &lt;span class="n"&gt;kubeCred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ServiceAccountCredential&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;CredentialsScope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GLOBAL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
      &lt;span class="s2"&gt;"Kubernetes service account"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;SystemCredentialsProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addCredentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;kubeCred&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;kubeCred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Starting in on setting up the Kubernetes support in Jenkins. The plugin declares
a special credential type that effectively loads on the fly from the pod’s service
account. But we still need to actually create that stub secret to feed into the
rest of config. I guess if we weren’t using a service account this would be
different, but service account are what the cool kids do.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;// Configure the cloud plugin.&lt;/span&gt;
  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Configuring Kubernetes cloud plugin'&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;cloud&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;KubernetesCloud&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'kubernetes'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;serverUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://kubernetes.default'&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kubeNamespace&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jenkinsUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://${labels['app']}:8080"&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jenkinsTunnel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${labels['app']}-agent:50000"&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credentialsId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kubeCred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;podTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PodTemplate&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt;
  &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${labels['release']}-agent"&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;containerTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ContainerTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jnlp'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"jenkins/jnlp-slave:$agentVersion"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;workingDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/home/jenkins'&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'${computer.jnlpmac} ${computer.name}'&lt;/span&gt; &lt;span class="c1"&gt;// Single quotes are intentional.&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;envVars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ContainerEnvVar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'JENKINS_URL'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jenkinsUrl&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resourceRequestCpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'200m'&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resourceLimitCpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'200m'&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resourceRequestMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'256Mi'&lt;/span&gt;
  &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resourceLimitMemory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'256Mi'&lt;/span&gt;
  &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clouds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clouds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then the actual cloud plugin configuration. This aims the plugin at the same
cluster as Jenkins is running inside of, and uses the pod’s service account
credentials as mentioned above. Then we set up a pod and container template
for the JNLP worker. This handles the communication with Jenkins once the worker
pod launches, but we’ll add additional containers to it in our pipeline libraries
to do the actual heavy lifting of the build. Those resource limits are based on
the Helm community chart for Jenkins and I’m not yet sure if they reflect reality
when Jenkins is under heavy load.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="c1"&gt;//////// PROJECT FOLDER&lt;/span&gt;
  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Creating organization folder'&lt;/span&gt;
  &lt;span class="c1"&gt;// Create the top-level item if it doesn't exist already.&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createProject&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrganizationFolder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MyName'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;// Set up GitHub source.&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;navigator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GitHubSCMNavigator&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;githubOrg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credentialsId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt; &lt;span class="c1"&gt;// Loaded above in the GitHub section.&lt;/span&gt;
  &lt;span class="n"&gt;navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;traits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Too many repos to scan everything. This trims to a svelte 265 repos at the time of writing.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;WildcardSCMSourceFilterTrait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'*-cookbook'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// We have a ton of old branches so try to limit to just master and PRs for now.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RegexSCMHeadFilterTrait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'^(master|PR-.*)'&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BranchDiscoveryTrait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Exclude branches that are also filed as PRs.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OriginPullRequestDiscoveryTrait&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Merging the pull request with the current target branch revision.&lt;/span&gt;
  &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;navigators&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;navigator&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This part I’m very proud of. The traditional way to automate Jenkins job
creation is the venerable Job DSL plugin. Job DSL uses its own Groovy scripting
API to create and manage jobs totally separately from the Jenkins Groovy
scripting framework, usually using a single “seed job” to create all the others.
This special DSL does (easily) support this use case, but I wanted to try and
avoid it. Having one fewer plugin to worry about, as well as a more tightly
integrated configuration seemed worth a bit of extra exploration. Building the
job configuration in pure Jenkins Groovy turned out to be pretty
straightforward other than being entirely undocumented, almost all of the
default values are actually what you want in this case. This block of code will
create the top-level folder and set up the GitHub folder source. As mentioned in the
comments, the organization scan was being too slow for my tastes so I set up
those two filter traits to cut down on the number of things Jenkins will even
bother checking for a Jenkinsfile. As I expand past just cookbook testing those
will probably go away, and the org scan only runs once a day so it being slow
isn’t actually a runtime problem, I was just being impatient in development.&lt;/p&gt;

&lt;p&gt;An aside about the GitHub plugin and webhooks. The plugin can automatically
configure the organization webhook for you, however I’m not actually doing that
here. That would require giving Jenkins an admin-capable token which I prefer
not to do. If you’re adapting this config to create dozens of organization folder,
maybe consider putting that back in (add &lt;code&gt;navigator.afterSave(folder)&lt;/code&gt; after the
&lt;code&gt;save()&lt;/code&gt; in the next snippet) but barring that I would just configure it manually.
You’ll want to set the URL to &lt;code&gt;https://myjenkinsserver.com/github-webhook/&lt;/code&gt; and
enable the &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull request&lt;/code&gt;, and &lt;code&gt;repository&lt;/code&gt; events.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Saving Jenkins config'&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We did it! I don’t think a manual save is actually required but it makes life
slightly easier if we have to &lt;code&gt;kubectl exec&lt;/code&gt; to log in and stare at the &lt;code&gt;config.xml&lt;/code&gt;
manually.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Scheduling GitHub organization scan'&lt;/span&gt;
  &lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="c1"&gt;// 30 seconds&lt;/span&gt;
    &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'--&amp;gt; Running GitHub organization scan'&lt;/span&gt;
    &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scheduleBuild&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Because we really wanted this to be fully hands-off, we schedule a org scan on
startup. This has to wait for a few other startup tasks inside Jenkins, so it
runs 30 seconds after this script.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s2"&gt;"--&amp;gt; configuring alti_jenkins... done"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Throwable&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'!!! Error configuring alti_jenkins'&lt;/span&gt;
  &lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;codehaus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;groovy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;StackTraceUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sanitize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;printStackTrace&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;println&lt;/span&gt; &lt;span class="s1"&gt;'!!! Shutting down Jenkins to prevent possible mis-configuration from going live'&lt;/span&gt;
  &lt;span class="n"&gt;jenkins&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cleanUp&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then finally the &lt;code&gt;catch&lt;/code&gt; for those runtime errors we talked about up at the
start. This will print the error to the log (ends up &lt;code&gt;kubectl logs&lt;/code&gt; or whatever
else you are using) and then tells Jenkins to shut down.&lt;/p&gt;

&lt;p&gt;And there you have it. A fully convergent configuration script to set up a
working Jenkins based on GitHub and Pipelines and all that jazz. Now we just
need to run this sucka’.&lt;/p&gt;

&lt;h3 id="helm-chart"&gt;Helm Chart&lt;/h3&gt;

&lt;p&gt;We started out first trying to use the &lt;a href="https://github.com/kubernetes/charts/tree/master/stable/jenkins"&gt;community Helm chart&lt;/a&gt;
for Jenkins directly, and then making a wrapper chart, but both approaches
turned out more complex than we wanted. At this time, my recommendation for
charts as complex as Jenkins is to fork them and use them as a starting point
for your own chart. As an example, the community chart uses the Jenkins
container image directly, and installs the config and plugins in an
initContainer, rather than building our image as shown above. We felt this
approach left too many moving pieces at deploy time and potentially compromised
our ability to do rollbacks if an upgrade didn’t go according to plan. I’m not
going to show the entire chart as there is a lot that is going to be specific to
my specific requirements but I do want to talk about the core of it.&lt;/p&gt;

&lt;h4 id="deployment"&gt;Deployment&lt;/h4&gt;

&lt;p&gt;As with most simple applications, the heart of the deployment is a Kubernetes
Deployment object, which manages the actual pods.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="l-Scalar-Plain"&gt;apiVersion&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;extensions/v1beta1&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;kind&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Deployment&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-jenkins"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We start with a pretty standard set of metadata for a Helm-created object. Some
of these values are used for selectors in the service/ingress side of things,
but mostly these are to aid in human debugging and management.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="l-Scalar-Plain"&gt;spec&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;replicas&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;1&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;strategy&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;type&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;RollingUpdate&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;selector&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;matchLabels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-jenkins"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Simple defaults, we only want one pod at a time because Jenkins’ idea of HA is
“restart it if it crashes” so leave things at that.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;  &lt;span class="l-Scalar-Plain"&gt;template&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-jenkins"&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;annotations&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;checksum/secret&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include (print $.Template.BasePath "/secret.yaml") . | sha256sum&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next the same metadata but for the Pod object this time. The annotation is
somewhat standard Helm pattern where we put a checksum of the rendered Secret
object in the Deployment so that when the Secret changes, the Deployment will
re-roll the pods automatically. This is needed because our config code only
looks at the secret data at container startup, so if they change after that it
will be ignored. The actual checksum isn’t used for anything, but it changing
will trigger Tiller (the server component of Helm) to update the Deployment,
which triggers the Pods to re-roll.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;    &lt;span class="l-Scalar-Plain"&gt;spec&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;serviceAccountName&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;span class="l-Scalar-Plain"&gt;-pull&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;securityContext&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# This is the default gid for the jenkins group in the upstream container.&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;fsGroup&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;1000&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Some general Pod configuration, setting the service account for Jenkins, the
image pull secret to talk to our internal registry to download the &lt;code&gt;alti_jenkins&lt;/code&gt;
image we made before, and setting the GID used for volume mounts down below so
that we can lock down the file modes a little bit just in case someone manages
to get a shell on the Jenkins container somehow.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;      &lt;span class="l-Scalar-Plain"&gt;containers&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;image&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.Server.Image&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}:{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.Server.ImageTag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="p-Indicator"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.Server.ImagePullPolicy&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.Server.ImagePullPolicy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="p-Indicator"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Basic container set up, nothing too interesting here. The only reason to change
the image pull policy is to set it to &lt;code&gt;Always&lt;/code&gt; in local development if I’m
rebuilding the same image version multiple times before I release it. But I
usually do my development in minikube anyway, so I use &lt;code&gt;minikube docker-env&lt;/code&gt; to
build directly in the Docker daemon used by Kubernetes later on:
&lt;code&gt;( eval "$(minikube docker-env)" &amp;amp;&amp;amp; docker build -t myrepo.com/alti_jenkins:2.whatever )&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;          ports:
            - containerPort: 8080
              name: http
            - containerPort: 50000
              name: agentlistener
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Expose two ports, one for HTTP and the other for JNLP workers. Unlike the
community chart, I didn’t see much reason to make these configurable since there
is no worry of port collisions or whatever.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;          &lt;span class="l-Scalar-Plain"&gt;resources&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
            &lt;span class="l-Scalar-Plain"&gt;requests&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;cpu&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.Server.Cpu&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;memory&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.Server.Memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We don’t yet really have a good idea for what these limits should be from production
data under heavy load, so just make them configurable for now.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;          &lt;span class="l-Scalar-Plain"&gt;volumeMounts&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
            &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;mountPath&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;/var/jenkins_home&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;jenkins-home&lt;/span&gt;
            &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;mountPath&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;/var/jenkins_secrets&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;jenkins-secrets&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;readOnly&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;true&lt;/span&gt;
            &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;downward&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;mountPath&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;/etc/downward&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;readOnly&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;true&lt;/span&gt;
     &lt;span class="-Error"&gt; &lt;/span&gt;&lt;span class="l-Scalar-Plain"&gt;volumes&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
      &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;jenkins-home&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;claimName&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.Persistence.ExistingClaim | default (include "alti-jenkins.fullname" .)&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
      &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;jenkins-secrets&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;secret&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;secretName&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;defaultMode&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;0440&lt;/span&gt;
      &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;downward&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;downwardAPI&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
          &lt;span class="l-Scalar-Plain"&gt;items&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
            &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;path&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;fieldRef&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
                &lt;span class="l-Scalar-Plain"&gt;fieldPath&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;metadata.labels&lt;/span&gt;
            &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;path&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;namespace&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;fieldRef&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
                &lt;span class="l-Scalar-Plain"&gt;fieldPath&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;metadata.namespace&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then the volumes. We need three volumes for three different purposes. The
big one is the JENKINS_HOME mount. Despite my best efforts towards immutable
configuration, Jenkins still does store a lot of state in the JENKINS_HOME
directory, like job history and build artifacts. As such, this needs to be
persistent at least over short timescales. If we lost this persistent volume we
could still trivially rebuild Jenkins, but we would lose enough history that it
might be frustrating. So for now, PVC.&lt;/p&gt;

&lt;p&gt;Then the two configuration volumes, a Secret volume and the Downward API volume.
As we saw back at the top of the &lt;code&gt;config.groovy&lt;/code&gt;, these are used to feed configuration
data into the Jenkins config. The &lt;code&gt;defaultMode&lt;/code&gt; settings works with &lt;code&gt;fsGroup&lt;/code&gt;
up above to slightly restrict things, though probably not in a way that really
matters but yay for defense in depth.&lt;/p&gt;

&lt;h4 id="secret"&gt;Secret&lt;/h4&gt;

&lt;p&gt;Mostly pretty rote, but including it here as an example:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="l-Scalar-Plain"&gt;apiVersion&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;v1&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;kind&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Secret&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;type&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Opaque&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;data&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;artifactory-token&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;required "Secrets.ArtifactoryToken is required" .Values.Secrets.ArtifactoryToken | b64enc | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;github-user&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;required "Secrets.GithubUser is required" .Values.Secrets.GithubUser | b64enc | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;github-token&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;required "Secrets.GithubToken is required" .Values.Secrets.GithubToken | b64enc | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;saml-keystore&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;required "Secrets.SamlKeystore is required" .Values.Secrets.SamlKeystore | nospace | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;saml-pass&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;required "Secrets.SamlPass is required" .Values.Secrets.SamlPass | b64enc | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="c1"&gt;# Not technically secret but convenient to put here because the same kind of code needs them.&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;development-mode&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;printf "%t" .Values.Server.DevelopmentMode | b64enc | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One thing to note is that the &lt;code&gt;SamlKeystore&lt;/code&gt; value is coming in already base64-encoded
because it’s a binary file format and it’s vastly easier to store it in the Helm
values file (and Tiller storage of the same) as text data. Given that we expect
this value to change infrequently (cert rotation, or in case of a security issue),
we just put it in base64 up front by hand.&lt;/p&gt;

&lt;h4 id="services"&gt;Services&lt;/h4&gt;

&lt;p&gt;We end up with two services, one each for HTTP and worker traffic.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="l-Scalar-Plain"&gt;apiVersion&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;v1&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;kind&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Service&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{.Release.Name}}-jenkins"&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;spec&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;ports&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;port&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;8080&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;targetPort&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;8080&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;http&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;selector&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{.Release.Name}}-jenkins"&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;and then:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="l-Scalar-Plain"&gt;apiVersion&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;v1&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;kind&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Service&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;span class="l-Scalar-Plain"&gt;-agent&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-jenkins"&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;spec&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;ports&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;port&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;50000&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;targetPort&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;50000&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;agentlistener&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;selector&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;component&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-jenkins"&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;type&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;ClusterIP&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id="ingress"&gt;Ingress&lt;/h4&gt;

&lt;p&gt;And finally an Ingress to handle TLS in production. We went with an Ingress
because we wanted to use &lt;a href="https://github.com/jetstack/kube-lego"&gt;&lt;code&gt;kube-lego&lt;/code&gt;&lt;/a&gt; to
automate certificates via LetsEncrypt. If you’re on AWS and want to use ACM
instead, you can do that directly via the first Service object above.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-yaml"&gt;&lt;span class="p-Indicator"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.Server.PublicHostname&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;apiVersion&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;extensions/v1beta1&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;kind&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;Ingress&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;metadata&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;name&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;labels&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;app&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;chart&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Chart.Version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;release&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;heritage&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Release.Service&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;annotations&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="l-Scalar-Plain"&gt;kubernetes.io/tls-acme&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="l-Scalar-Plain"&gt;spec&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;tls&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;secretName&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;span class="l-Scalar-Plain"&gt;-tls&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;hosts&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.Server.PublicHostname | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
  &lt;span class="l-Scalar-Plain"&gt;rules&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
    &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;host&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.Server.PublicHostname | quote&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
      &lt;span class="l-Scalar-Plain"&gt;http&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
        &lt;span class="l-Scalar-Plain"&gt;paths&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
          &lt;span class="p-Indicator"&gt;-&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;path&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;/&lt;/span&gt;
            &lt;span class="l-Scalar-Plain"&gt;backend&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;serviceName&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="p-Indicator"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "alti-jenkins.fullname" .&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;
              &lt;span class="l-Scalar-Plain"&gt;servicePort&lt;/span&gt;&lt;span class="p-Indicator"&gt;:&lt;/span&gt; &lt;span class="l-Scalar-Plain"&gt;8080&lt;/span&gt;
&lt;span class="p-Indicator"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="p-Indicator"&gt;}}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This will only be active on production, for local development I just talk
directly to the service via minikube’s &lt;code&gt;minikube service&lt;/code&gt; helper command. I’ve
only tested with the Nginx ingress controller, but I would imagine it should
work with the GCE ingress too, and any other controller supported by &lt;code&gt;kube-lego&lt;/code&gt;.
In production we installed both &lt;code&gt;nginx-ingress&lt;/code&gt; and &lt;code&gt;kube-lego&lt;/code&gt; using
their community Helm charts directly:&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-bash"&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;helm install -n nginx-ingress stable/nginx-ingress
&lt;span class="nv"&gt;$ &lt;/span&gt;helm install --set config.LEGO_EMAIL&lt;span class="o"&gt;=&lt;/span&gt;me@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  --set config.LEGO_URL&lt;span class="o"&gt;=&lt;/span&gt;https://acme-v01.api.letsencrypt.org/directory &lt;span class="se"&gt;\&lt;/span&gt;
  -n kube-lego stable/kube-lego&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id="global-pipeline-libraries"&gt;Global Pipeline Libraries&lt;/h3&gt;

&lt;p&gt;Okay, so we have a working, running Jenkins server. Progress! Next step is to
get some actual builds on it. Because we have hundreds of cookbooks which should
all use the same build logic, we wanted to make sure all of that was kept
somewhere centralized. Jenkins’ global libraries system made this very easy,
though as I’ve only attacked the cookbook testing use case I don’t actually
have very much yet.&lt;/p&gt;

&lt;p&gt;An aside about how to structure this: each helper goes in a file named
&lt;code&gt;vars/nameOfHelper.groovy&lt;/code&gt;. The bit after &lt;code&gt;vars/&lt;/code&gt; is what ends up being the
function name for your Jenkinsfiles.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;&lt;span class="c1"&gt;// vars/altiNode.groovy&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Closure&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;secretsRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'JENKINS_SECRETS'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'/var/jenkins_secrets'&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;artifactoryToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$secretsRoot/artifactory-token"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;trim&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

  &lt;span class="n"&gt;withEnv&lt;/span&gt;&lt;span class="o"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'CI=true'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"BERKSHELF_PATH=${env.WORKSPACE}/.berkshelf"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ARTIFACTORY_API_KEY=$artifactoryToken"&lt;/span&gt;&lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cookbook'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'alti-pipeline'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;First a utility helper to help avoid too many levels of indentation in other
helpers. This is like the build in &lt;code&gt;node {}&lt;/code&gt; pipeline step but with some standard
stuffs for our testing environment.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;&lt;span class="c1"&gt;// vars/altiCookbook.groovy&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Closure&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;altiPipelineVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'4.9.2'&lt;/span&gt;

    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;downwardRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DOWNWARD_VOLUME'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'/etc/downward'&lt;/span&gt;

    &lt;span class="c1"&gt;// Parse the labels test.&lt;/span&gt;
    &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[:]&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"$downwardRoot/labels"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;eachLine&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'='&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Next is the interesting one, the cookbook testing pipeline, though currently
a very simple one. First we do some configuration stuff like we saw in &lt;code&gt;config.groovy&lt;/code&gt;.
It is, in fact, a copy-pasta because I couldn’t find a reasonable way to share
code between the two contexts and it’s not very long anyway.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;    &lt;span class="n"&gt;podTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;label:&lt;/span&gt; &lt;span class="s1"&gt;'cookbook'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;imagePullSecrets:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${labels['app']}-pull"&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt; &lt;span class="nl"&gt;containers:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;containerTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'alti-pipeline'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;image:&lt;/span&gt; &lt;span class="s2"&gt;"altiscale-docker-dev.jfrog.io/alti_pipeline:${altiPipelineVersion}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;alwaysPullImage:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;command:&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh -c \"trap 'exit 0' TERM; sleep 2147483647 &amp;amp; wait\""&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="o"&gt;])&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then we set up the pod for our job to build in. This will be combined with the
podspec we gave the Kubernetes cloud plugin up in the Jenkins configuration so
the final pod will end up with two containers, one for the JNLP worker and another
with the build environment image. The build environment doesn’t actually have
a service to run, so use the sleep wait to keep it busy until the pod is shut down.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;integrationTests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Check'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;altiNode&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
                &lt;span class="c1"&gt;// Check that we have an acceptable version of alti_pipeline, just looks at the major version.&lt;/span&gt;
                &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;gemfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Gemfile'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gemfile&lt;/span&gt; &lt;span class="o"&gt;=~&lt;/span&gt; &lt;span class="s"&gt;/gem.*alti_pipeline.*\b${altiPipelineVersion[0]}\./&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Gemfile is compatible with alti_pipeline ${altiPipelineVersion}"&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="s2"&gt;"Gemfile is not compatible with alti_pipeline ${altiPipelineVersion}:\n"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;gemfile&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="c1"&gt;// Parse out the integration tests for use in the next stage.&lt;/span&gt;
                &lt;span class="n"&gt;integrationTests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;script:&lt;/span&gt; &lt;span class="s1"&gt;'kitchen list --bare'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;returnStdout:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first stage is a sanity check. Unlike many Chef shops, we don’t actually use
ChefDK (that would have to be a whole ‘nother blog post so just take it as a given)
and instead have a Gemfile in each cookbook that points it at our equivalent gem,
&lt;code&gt;alti_pipeline&lt;/code&gt;. Here we want to make sure that the cookbook’s Gemfile is the
same major version as the build image since if it isn’t, the build is very
unlikely to work. We also grab the list of all Test Kitchen instances to build in
the next stage.&lt;/p&gt;

&lt;pre&gt;&lt;code class="language-groovy"&gt;        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;testJobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'Lint'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;altiNode&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
                        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'rm -f Gemfile Gemfile.lock'&lt;/span&gt;
                        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'rake style'&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;},&lt;/span&gt;
                &lt;span class="s1"&gt;'Unit Tests'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;altiNode&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
                        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'rm -f Gemfile Gemfile.lock'&lt;/span&gt;
                            &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'rake spec'&lt;/span&gt;
                        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;junit&lt;/span&gt; &lt;span class="s1"&gt;'results.xml'&lt;/span&gt;
                        &lt;span class="o"&gt;}&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;},&lt;/span&gt;
            &lt;span class="o"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;integrationTests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
              &lt;span class="n"&gt;testJobs&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Integration $instance"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;altiNode&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
                        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'rm -f Gemfile Gemfile.lock'&lt;/span&gt;
                        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"kitchen test --destroy always $instance"&lt;/span&gt;
                    &lt;span class="o"&gt;}&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testJobs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;

    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And then finally the actual test bit of the pipeline. This sets up jobs for
lint checking, unit tests, and one job each for the integration tests so they
can all run in parallel. You can see this uses the &lt;code&gt;altiNode&lt;/code&gt; helper from above,
instead of the usual &lt;code&gt;node&lt;/code&gt; pipeline step. We’re also removing the Gemfile in-place
since we’ve already installed all the needed gems in my build environment image
and don’t want bundler to even try and activate.&lt;/p&gt;

&lt;h3 id="cookbook-integration-testing"&gt;Cookbook Integration Testing&lt;/h3&gt;

&lt;p&gt;As part of this project I also built a new Test Kitchen driver, &lt;a href="https://github.com/coderanger/kitchen-kubernetes"&gt;&lt;code&gt;kitchen-kubernetes&lt;/code&gt;&lt;/a&gt;,
specifically for running Chef cookbook integration tests on top of Kubernetes.
This works similarly to &lt;code&gt;kitchen-docker&lt;/code&gt; and &lt;code&gt;kitchen-dokken&lt;/code&gt;, but using
Kubernetes machinery rather than plain Docker containers. If duplicating this
set up for yourself, make sure you remember to include &lt;code&gt;rsync&lt;/code&gt; in the job build
image (&lt;code&gt;alti_pipeline&lt;/code&gt; above) as that is required for &lt;code&gt;kitchen-kubernetes&lt;/code&gt;’s
file upload system.&lt;/p&gt;

&lt;h3 id="build-environment-image"&gt;Build Environment Image&lt;/h3&gt;

&lt;p&gt;While most people doing Chef cookbook testing should probably use the &lt;a href="https://hub.docker.com/r/chef/chefdk/"&gt;&lt;code&gt;chef/chef-dk&lt;/code&gt;&lt;/a&gt;
image, as mentioned before we are not using ChefDK for our environment management.
The short version of “why” is that we’re still on Chef 12 but wanted newer versions
of a lot of tools, as well as including a lot of our own utility gems. We may
yet transition back to ChefDK but for now we needed to create a container image
that included a bunch of private gems. Pulling in private gems means including
an access token for the repository (careful readers have probably figured out
by now that we use Artifactory), but unfortunately build-time secrets are still
a notable problem with &lt;code&gt;docker build&lt;/code&gt;. There are a few options, short-lived tokens
that do get baked in to the image but are already expired by the time anyone
could get them, localhost proxies that handle authentication, use of alternative
image build systems like &lt;a href="https://habitat.sh/"&gt;Habitat&lt;/a&gt;/&lt;a href="https://github.com/projectatomic/buildah"&gt;buildah&lt;/a&gt;, but we
decided to try and keep it simple and use the new “squash build” feature in Docker.&lt;/p&gt;

&lt;p&gt;We decided to use &lt;code&gt;alpine&lt;/code&gt; as the base image (shoutout to the great folks at
&lt;a href="https://gliderlabs.github.io/devlog/"&gt;Glider Labs&lt;/a&gt;) to minimize the file size. Kubernetes does cache images
aggressively, but every little bit helps in improving build performance. The
final &lt;code&gt;Dockerfile&lt;/code&gt; looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;FROM alpine:latest
ENV VERSION=4.9.2
ENV ALTISCALE_KITCHEN_KUBERNETES=true
ENV ALTISCALE_BERKS_ARTIFACTORY=true
COPY .gemrc /root
RUN set -x &amp;amp;&amp;amp; \
    apk --update-cache add build-base ruby-io-console ruby ruby-dev libffi libffi-dev zlib zlib-dev curl git openssh-client rsync &amp;amp;&amp;amp; \
    gem install alti_pipeline -v $VERSION &amp;amp;&amp;amp; \
    git clone https://github.com/coderanger/kitchen-kubernetes /tmp/kitchen-kubernetes &amp;amp;&amp;amp; \
    ( cd /tmp/kitchen-kubernetes &amp;amp;&amp;amp; gem build *.gemspec &amp;amp;&amp;amp; gem install --local *.gem ) &amp;amp;&amp;amp; \
    curl -L -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl &amp;amp;&amp;amp; \
    chmod +x /usr/local/bin/kubectl &amp;amp;&amp;amp; \
    gem sources --clear-all &amp;amp;&amp;amp; \
    rm -rf /root/.gemrc /usr/lib/ruby/gems/2.4.0/cache/*.gem /tmp/kitchen-kubernetes &amp;amp;&amp;amp; \
    for f in /usr/lib/ruby/gems/2.4.0/gems/*; do rm -rf $f/spec $f/test $f/examples $f/distro $f/acceptance; done &amp;amp;&amp;amp; \
    apk del build-base ruby-dev libffi-dev zlib-dev curl
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This can be broken down in to four parts. First the base image and some
environment variables we want set for all builds. Then copying the gem server
credentials, from this point on things become radioactive because we have a
secret value in the image. Then the installs, first a bunch of Alpine packages
we need, then the top-level &lt;code&gt;alti_pipeline&lt;/code&gt; gem, &lt;code&gt;kitchen-kubernetes&lt;/code&gt; (from git
because I haven’t actually put up a release yet), and  &lt;code&gt;kubectl&lt;/code&gt; (for use by
&lt;code&gt;kitchen-kubernetes&lt;/code&gt;). Finally a whole bunch of cleanup. This is mostly to
reduce the final image size, removing files and packages we don’t need after
image creation. But also we remove the &lt;code&gt;.gemrc&lt;/code&gt;, making the image no longer
radioactive if built correctly.&lt;/p&gt;

&lt;p&gt;Even with this &lt;code&gt;COPY&lt;/code&gt;+&lt;code&gt;rm&lt;/code&gt; though, we need to make sure to build the image using
&lt;code&gt;docker build --squash&lt;/code&gt; (which requires experimental features be enabled on the
Docker daemon, add &lt;code&gt;--experimental=true&lt;/code&gt; to the daemon command line). If built
without &lt;code&gt;--squash&lt;/code&gt;, the final image would &lt;em&gt;look&lt;/em&gt; like it doesn’t have the token,
but it would still be visible in the intermediary layer created between the
&lt;code&gt;COPY&lt;/code&gt; and &lt;code&gt;RUN&lt;/code&gt;. Hopefully at some point there will be a better solution for
build-time secrets, but for now this is enough to get us a build environment
weighing in at around 100MB.&lt;/p&gt;

&lt;h3 id="per-repo-jenkinsfile"&gt;Per-Repo Jenkinsfile&lt;/h3&gt;

&lt;p&gt;One requirement of this set up is you do need to put a Jenkinsfile in each
repository you want to be built. This might be frustrating for some, having to
touch every repo when that could potentially be (and in my case, is) hundreds
of projects. That said, currently the Jenkinsfile we are adding to each repo is
literally &lt;code&gt;altiCookbook { }&lt;/code&gt;. So it’s not much in terms of footprint, but you
do have to do the legwork, either by hand or via a script using the GitHub API.&lt;/p&gt;

&lt;h3 id="to-conclude"&gt;To Conclude&lt;/h3&gt;

&lt;p&gt;As I mentioned at the start, my goal here is to provide a jump start in designing
your own Jenkins deployment. I suspect the precise combo of design choices shown
here might be literally unique in the world, but most of the bits are very
modular and the overall structure should be a starting point for your own
specifics.&lt;/p&gt;

&lt;p&gt;If you have any questions on any of this code or the design decisions behind it
you can reach me at &lt;a href="mailto:noah@coderanger.net"&gt;noah@coderanger.net&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>tag:coderanger.net,2017-09-25:/react-patents/</id>
    <title type="html">React and Patents</title>
    <published>2017-09-25T04:00:00Z</published>
    <updated>2017-09-25T17:01:27Z</updated>
    <link rel="alternate" href="https://coderanger.net/react-patents/"/>
    <content type="html">&lt;p&gt;Over the past several weeks there has been an evolving discussion about the
licensing of &lt;a href="https://facebook.github.io/react/"&gt;React&lt;/a&gt;, a popular library for
building complex user interfaces. There has been a lot of conflicting information
posted about the state of React, what kind of licensing terms are involved, and
why all of this matters. I would like to try and set the record straight to the
best of my ability. But first:&lt;/p&gt;

&lt;h2 id="a-disclaimer"&gt;A Disclaimer&lt;/h2&gt;

&lt;p&gt;I am not a lawyer and nothing in this post should be considered legal advice.
If you have any specific questions about any of this, please talk to a licensed
intellectual property attorney in your local jurisdiction. This is for
entertainment purposes only. This is just my opinions/knowledge, not those
of my employer, Facebook, the Apache Software Foundation, or anyone else.&lt;/p&gt;

&lt;p&gt;Okay, with the disclaimer out of the way, let’s rewind, where did all this start?&lt;/p&gt;

&lt;h3 id="the-story-up-until-now"&gt;The Story Up Until Now&lt;/h3&gt;

&lt;p&gt;While we could start way back with the initial creation of React, that was a
long time ago and the part of the story we all care about begins in April 2017.
In a somewhat unassuming Jira ticket, &lt;a href="https://issues.apache.org/jira/browse/LEGAL-303"&gt;one of the Cassandra team asked the Apache
Software Foundation legal affairs team if directly integrating with RocksDB was
acceptable&lt;/a&gt;. I hear you say
“RocksDB? I thought this was about React!” well that was brought up soon after
in the same ticket, as both RocksDB and React were made available under the same
license. This was resolved a few months later in July when the ASF legal team
made the call that the license terms used for RocksDB (and therefore React) were
incompatible with the requirements on ASF projects. This was quickly picked up
by the media and run with headlines like &lt;a href="https://www.theregister.co.uk/2017/07/17/apache_says_no_to_facebook_code_libraries/"&gt;“Apache says ‘no’ to Facebook code
libraries”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’ll come back to RocksDB later, but with React this kicked off a firestorm of
concerned and confused users asking what was going on. There was some discussion
for a few weeks as to &lt;a href="https://github.com/facebook/react/issues/10191"&gt;if React should be changed to a different license&lt;/a&gt;, but
in mid-August this was closed with Facebook &lt;a href="https://code.facebook.com/posts/112130496157735/explaining-react-s-license/"&gt;confirming that they felt the current
license was appropriate&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then, unexpectedly, a few days ago (September 22nd) Facebook announced they would
be &lt;a href="https://code.facebook.com/posts/300798627056246/relicensing-react-jest-flow-and-immutable-js/"&gt;relicensing React and several related projects&lt;/a&gt;
to use the MIT license.&lt;/p&gt;

&lt;p&gt;Okay, so that’s the whole history, let’s look at some major questions that came
out of these few months:&lt;/p&gt;

&lt;h3 id="who-owns-react-and-rocksdb"&gt;Who Owns React (and RocksDB)?&lt;/h3&gt;

&lt;p&gt;To get it out of the way, code is in general owned by whomever writes it. If you
write code as a part of your job, it is owned by your employer, give or take what
is in your employment contract. Facebook does require a contributor license agreement (CLA)
for their projects, and the CLA grants Facebook some strongly worded rights for
copyright use and patent access, but in the end each contributor remains the
owner of the copyright in their contribution. This means that while React is
managed by Facebook, and they do own a lot of the copyright over their projects,
in the end they are actually owned by the collective community of developers that
works on them. In the case of RocksDB specifically there is also a large chunk
of the code which is owned by Google, as it is an outgrowth of an earlier Google
project called LevelDB.&lt;/p&gt;

&lt;p&gt;Ownership is generally thought of in terms of copyright ownership, as that’s the
more directly relevant bit, but another factor in the control of software is
who owns any patents which cover the software. We’ll leave this part for later
in this post as it’s a much more complex topic.&lt;/p&gt;

&lt;h3 id="what-was-the-original-license-for-react-and-rocksdb"&gt;What Was The Original License For React (and RocksDB)?&lt;/h3&gt;

&lt;p&gt;The license used originally by Facebook has two main parts, a copyright license
and a patent license. The copyright license half is pretty straightforward, a
3-Clause BSD license. This is part of a family of minimalist copyright licenses
that allow use, copying, and modification of the code. Without this license, those
rights would remain exclusively with the copyright holders (i.e. the person or
company which wrote each section of the code). The &lt;a href="https://opensource.org/licenses/BSD-3-Clause"&gt;BSD license&lt;/a&gt; (usually in 2
or 3-clause variants, sometimes 4) is fairly well known and the requirements you
must meet to use those licensed rights (preserve the copyright notice and license,
don’t use Facebook’s name in endorsements, don’t sue anyone because the code doesn’t
work) are quite reasonable.&lt;/p&gt;

&lt;p&gt;The second half is a patent license, which says that
anyone using the software is allowed to use any Facebook patents that might cover
that software so Facebook cannot sue them for patent infringement. The goal here
is the same as with the copyright license, making sure users have enough rights
to be successful open-source citizens while protecting Facebook and the other
rightsholders involved.&lt;/p&gt;

&lt;p&gt;Facebook has termed this a “BSD+Patents” license, though this should not be
confused with the OSI-approved license also called “BSD+Patent” (or sometimes
BSD-2-Clause-Patent) as the two are unrelated.&lt;/p&gt;

&lt;h3 id="what-problem-did-the-asf-have-with-bsdpatents"&gt;What Problem Did The ASF Have With BSD+Patents?&lt;/h3&gt;

&lt;p&gt;As mentioned in the previous section, the use of the BSD license for copyrights
was 100% okay with everyone, the issues all revolved around the patent license.
This was something created by Facebook specifically, so to start with it was not
a well-known quantity in the same way as things like the BSD or MIT license
would be. While using standardized licenses (or more specifically, licenses recognized
by the &lt;a href="https://opensource.org/licenses/alphabetical"&gt;Open Source Initiative&lt;/a&gt;)
does certainly make life easier for open-source lawyers, it isn’t a specific
requirement.&lt;/p&gt;

&lt;p&gt;The Apache Software Foundation does its best to make the projects under its
umbrella be somewhat uniform from a legal perspective. If you get a legal “okay”
to use one of them, you can almost certainly use any other ASF project. This is
important for ecosystem stability as many ASF projects depend on other ASF projects
to function. We’ll get to the Apache-2.0 license in more detail later, but the
ASF created it to help in this mission, to provide a unified set of licensing terms
for all ASF projects. But the open-source ecosystem is a big place, and many
ASF projects want to depend on libraries not from the ASF and thus sometimes
under different license terms. So the doctrine used by the ASF legal team is that
other licenses are okay as long as they do not put any more requirements on the
end user than the Apache-2.0 license does. So for example, pretty much all software
in the world depends on zlib, which is distributed under the &lt;a href="https://www.zlib.net/zlib_license.html"&gt;zlib license&lt;/a&gt;,
but from the point of view of the ASF crew, that is less restrictive than
Apache-2.0 so it doesn’t make life any harder for end users.&lt;/p&gt;

&lt;p&gt;The problematic section of the patent license was under what conditions
the license would be revoked. While most copyright licenses are given as irrevocable,
it is normal for patent licenses to include a termination clause in the case of
a lawsuit over the software. This helps to protect the company, if they get sued
they can at least prevent you from continuing to benefit from their IP for the
duration of the lawsuit. But that said, the Facebook patent license &lt;a href="https://github.com/facebook/react/blob/b8ba8c83f318b84e42933f6928f231dc0918f864/PATENTS"&gt;went a lot
further&lt;/a&gt;:
“The license granted hereunder will terminate, automatically and without notice,
if you … initiate … any Patent Assertion against Facebook or any of its subsidiaries or corporate
affiliates”. So rather than being scoped to just lawsuits over the specific
project, any patent suit brought against Facebook would terminate the patent
license. The ASF Legal team decided this represented a restriction above and
beyond the Apache-2.0 license, and so it could not be used with ASF projects.&lt;/p&gt;

&lt;p&gt;It should be repeated that this was a decision by the ASF Legal team for ASF-controlled
projects, that’s it. This was not an abstract judgment that Apache-2.0 code can’t
be used with BSD+Patents in general, nor was it any comment on the quality of
Facebook’s code or on their patent license as being good or bad. It was specifically
to ensure the licensing goals of the ASF remained safe, nothing more.&lt;/p&gt;

&lt;h3 id="so-what-happened-to-rocksdb"&gt;So What Happened To RocksDB?&lt;/h3&gt;

&lt;p&gt;In response to the original decision from ASF Legal, the RocksDB team &lt;a href="https://github.com/facebook/rocksdb/pull/2589"&gt;quickly
switched from BSD+Patents to Apache-2.0&lt;/a&gt;.
There isn’t a lot of public information on how this change went down or why, but
given it happened within a few hours I’m guessing it was considered fairly uncontroversial
by everyone involved. As mentioned earlier, there is a lot of code from LevelDB
still in use, which is owned by Google and will continue to be made available
under the terms of the 3-Clause BSD license.&lt;/p&gt;

&lt;p&gt;One slight addendum, since this will come up again with React: I’m not actually
100% what the legal basis for the change of license was. Normally you need
approval from all copyright holders before changing a license, but it is possible
the Facebook legal team decided the sublicensing requirement of the Facebook CLA
made it okay to relicense in this fashion without getting approval. Or it’s
possible they just figured everyone would be okay with it, which is probably
correct.&lt;/p&gt;

&lt;h3 id="why-did-facebook-want-the-broad-termination-clause"&gt;Why Did Facebook Want The Broad Termination Clause?&lt;/h3&gt;

&lt;p&gt;Without getting too far in to the details, one major problem facing the software
industry right now is a plethora of patent lawsuits from companies that buy out
very broad or wide-reaching patents and then use them to try and extract settlements
from a large number of software companies by threating litigation. These are
colloquially called “patent trolls”. Regardless of your personal feelings on the
issue, Facebook considers this a threat to their business and was using the broad
termination clause across all of their open-source software to try and take some
wind out of the sails of future trolls as if the troll was using anything under
the Facebook patent license, if they filed a lawsuit over one of the troll patents,
Facebook could counter-sue using one of theirs.&lt;/p&gt;

&lt;p&gt;This defensive measure was deemed important enough to reject the initial request
for relicensing React. And speaking personally, I totally understand that impulse.
I agree that troll lawsuits are a chilling scourge on our industry and in general
I support legal defense machinery to try and reduce them.&lt;/p&gt;

&lt;h3 id="why-did-other-people-not-want-the-broad-termination-clause"&gt;Why Did Other People Not Want The Broad Termination Clause?&lt;/h3&gt;

&lt;p&gt;While you might initially think “but I would never sue Facebook”, things are
often more complex than that. For other large companies, this one-sided termination
clause effectively means the company as a whole is giving up the right to pursue
legitimate patent infringement cases against Facebook. Given the scope and scale
of many patent portfolios, this represents an unacceptable risk to many (already risk-averse)
legal departments.&lt;/p&gt;

&lt;p&gt;For smaller companies, especially VC-backed startups, suing Facebook would likely
have never been an option in the first place even if Facebook was very clearly
infringing as the legal costs can be immense. However the other shoe dropping
is that many startups want to end up getting acquired by larger companies, so
all the same issues get pulled back in. While I don’t know of any examples
citing React specifically, acquisition deals fall apart all the time over intellectual
property concerns so this is something even startups should take seriously.&lt;/p&gt;

&lt;p&gt;That said, for some cases it really is okay. I personally used React for
some projects, like the dashboard that powers my home information system, when
it was under the BSD+Patents license.&lt;/p&gt;

&lt;h3 id="so-if-react-isnt-bsdpatents-anymore-what-happened"&gt;So If React Isn’t BSD+Patents Anymore, What Happened?&lt;/h3&gt;

&lt;p&gt;While the public discussion about relicensing React ended after the first 4 weeks,
apparently it continued within Facebook, for which they should be recognized and
commended. This resulted in a somewhat surprising announcement that React will switch
from BSD+Patents to the MIT license. At the time of writing, I don’t
think this change has been made, but they are planning to implement it soon. As
with the RocksDB license change, I’m unsure of the exact legal mechanism being
used, especially since this is moving to a less restrictive license (i.e. the
copyright holders are giving up more rights). But regardless, we can expect this
to be reality soon and the rest of this post assumes Facebook will follow through
on the license change.&lt;/p&gt;

&lt;h3 id="awesome-this-means-everything-is-fine-now-right"&gt;Awesome, This Means Everything Is Fine Now, Right?&lt;/h3&gt;

&lt;p&gt;Maybe. Here is where things start getting dicey. The MIT license is generally
thought of as a copyright license, just like the BSD licenses. With the switch
to it, this would mean Facebook is intending to terminate (or at least no longer
rely on) their patent license. But that doesn’t make Facebook’s patents go away.&lt;/p&gt;

&lt;p&gt;A quick check on &lt;a href="https://patents.google.com/?assignee=Facebook%2c+Inc."&gt;Google Patents&lt;/a&gt;
shows Facebook currently controls around 5200 patents (not even counting their subsidiaries). Without going through
every single one of them, there is no good way to know which, if any, would
apply to React or related projects. The posture Facebook initially took around
their patent license certainly intimated that they think one or more of their
patents does apply, but they don’t have to actually show their hand until/unless
they actually file a lawsuit against someone the first time.&lt;/p&gt;

&lt;p&gt;So if we are now ignoring the Facebook patent license, there are three scenarios.
One is that the whole thing was a bluff (perhaps unintentionally) and there are
no Facebook patents which cover React, or equivalently Facebook thinks any
patents they do have would be indefensible in a &lt;a href="https://en.wikipedia.org/wiki/Alice_Corp._v._CLS_Bank_International"&gt;post-Alice&lt;/a&gt;
world. Second, React will now be covered by a copyright license, but all patents
covering React (and here we assume there are some) will not be licensed and Facebook
can sue anyone using React at any time. Third, Facebook is relying on a somewhat-controversial
reading of the MIT license that says it does provide a patent license.&lt;/p&gt;

&lt;p&gt;The first case seems unlikely. The second is downright evil and I have more trust
in both the Facebook open-source folks and the React team than that. This leaves
us with option three, further bolstered by a &lt;a href="https://twitter.com/dan_abramov/status/911508180894720000"&gt;tweet from one of the React team&lt;/a&gt;.
This reading of the MIT license revolves around the phrase “including without
limitation the rights to use”. Some people claim given the broad wording of this
rights grant, it would include both copyrights and patent rights. But this is
the aforementioned dicey bit. To the best of my knowledge, an implicit patent
grant this vague and outside of a commercial context has never been tested in court.
If this is the goal of the Facebook team, it would also mean they are effectively
permanently renouncing those patents, as the implied grant would have no termination
rules, even for a lawsuit directly about React. I would very much like to believe
this case is our reality, but without a more well-known legal footing, I’m very
concerned we may have inadvertently created a scenario two where everyone is
actually just infringing all the time.&lt;/p&gt;

&lt;p&gt;The announcement of the relicensing from Facebook was very notably absent any
discussion of patent rights, and all postings I’ve seen from Facebook folks about
this have been very carefully worded to avoided specifically stating what Facebook
thinks the patent rights situation will be.&lt;/p&gt;

&lt;h3 id="but-you-said-rocksdb-relicensed-and-everything-was-fine"&gt;But You Said RocksDB Relicensed And Everything Was Fine?&lt;/h3&gt;

&lt;p&gt;RocksDB switched to the Apache-2.0 license. Like many newer licenses, it
includes specific provisions covering both copyright rights and patent rights
(and more). This means that patent rights around RocksDB might actually be more
restrictive than React, but they are far more explicit and well understood.&lt;/p&gt;

&lt;p&gt;Specifically the Apache-2.0 license uses a fairly standard termination clause,
where the lawsuit must claim “the Work or a Contribution incorporated within the
Work” is what is infringing. As mentioned earlier, this simplifies lawsuits over
the project itself while not totally disarming patent litigation about unrelated
things. Beyond just the patent licensing, the Apache-2.0 license also makes a
much more specific declaration of which copyright-related rights are being shared,
covers trademark licensing, and makes the rules for redistribution clearer.&lt;/p&gt;

&lt;p&gt;The fact that Facebook chose the MIT license for React instead of Apache-2.0
sends a signal that they may be trying to hedge their bets here in a way the
RocksDB team did not.&lt;/p&gt;

&lt;h3 id="okay-can-i-just-use-preact-instead"&gt;Okay, Can I Just Use Preact Instead?&lt;/h3&gt;

&lt;p&gt;Again, maybe. While copyright law has a concept of “derived works” vs.
“non-derived works”, patent law (in this case) does not. Preact is (I think)
built as a whitebox reimplementation of many React APIs in a way that Facebook
doesn’t hold any copyright over it. But patents don’t care about that, if Preact
implements an algorithm or process patented by Facebook it doesn’t matter if it
was created independently or not. But since we don’t know which of their
thousands of patents might apply, there is no good way to say if Preact would
be infringing or not.&lt;/p&gt;

&lt;h3 id="tldr-what-do-i-do"&gt;Tl;Dr What Do I Do?&lt;/h3&gt;

&lt;p&gt;At this time, I still think it would be wise for companies to avoid React. If
at some point in the future Facebook provides more specific guidance about patent
rights this could be easily remedied, but only time will tell on that. I do
firmly believe that Facebook and the React team are not being intentionally
bad actors in all this, and the open-source community should give them the
benefit of the doubt when it comes to incomplete explanations, but at the same
time we each need to ensure the legal safety of our own companies and projects.&lt;/p&gt;

&lt;p&gt;If you found this discussion of intellectual property issues interesting, I’ve
got a whole talk about the basics of IP which can you &lt;a href="/talks/ip/"&gt;check out the slides for&lt;/a&gt;
or come see the talk at &lt;a href="https://2017.pygotham.org/"&gt;PyGotham&lt;/a&gt; or &lt;a href="https://www.devopsdays.org/events/2017-hartford/welcome/"&gt;DevOpsDays Hartford&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As I said at the start, I am not an attorney so if you have specific questions
about React or IP law in general I’m happy to do my best to help but please also
talk to a lawyer or your company’s legal department if you have one.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>tag:coderanger.net,2017-07-26:/chef-telemetry/</id>
    <title type="html">Gathering Chef Usage Data</title>
    <published>2017-07-26T04:00:00Z</published>
    <updated>2017-07-26T22:26:41Z</updated>
    <link rel="alternate" href="https://coderanger.net/chef-telemetry/"/>
    <content type="html">&lt;p&gt;A &lt;a href="https://github.com/chef/chef-rfc/pull/269"&gt;proposed Chef RFC&lt;/a&gt; currently
sitting in the review queue revolves around gathering anonymized usage information
about Chef and some of the Chef ecosystem tools. I think this proposal needs
some more eyeballs on it from the Chef community so hopefully this will encourage
some of you to weigh in by commenting on the pull request. Usual disclaimer on
time-sensitive posts, if you found this via Google some time in the future,
it might not apply anymore.&lt;/p&gt;

&lt;h2 id="why-gather-user-data"&gt;Why gather user data?&lt;/h2&gt;

&lt;p&gt;As both a Chef maintainer and a cookbook developer, I have an interest in this
user data. A big problem for Chef, as with almost all open-source projects, has
been that we know almost nothing about how the things we write actually get used.
We have some signaling from people asking questions on Slack or filing bug
reports, but this is both very coarse-grained and ignores a huge majority of
users. This makes it very hard to know how to allocate time and resources, both
of which are in short supply (again, same as in any other open-source project,
none of this is unique to Chef).&lt;/p&gt;

&lt;p&gt;Having some kind of automated data collection would give us a huge boost in
knowing what things are popular and should be given more attention, and which
were maybe failed experiments or are broken in some way we haven’t been made
aware of. This has happened many times over the years, we maintainers assuming
something was no longer used, and finding out only after the release that we were
wrong (&lt;a href="https://xkcd.com/1172/"&gt;obligatory xkcd&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;As a concrete example, Test Kitchen supports a lot of driver plugins. There are
two Docker-based drivers, my &lt;code&gt;kitchen-docker&lt;/code&gt; and Sean’s &lt;code&gt;kitchen-dokken&lt;/code&gt;. We’ve
been trying to steer people towards Dokken as a better user experience for
simple use cases, but as the maintainer of &lt;code&gt;kitchen-docker&lt;/code&gt; I have no way to
judge how common the complex or advanced cases are, or even the rough distribution
of users for &lt;code&gt;docker&lt;/code&gt; vs &lt;code&gt;dokken&lt;/code&gt;. This makes it very hard to know what kinds
of things to support in each and (probably) leads to worse software for everyone.&lt;/p&gt;

&lt;h2 id="why-not-to-gather-user-data"&gt;Why not to gather user data?&lt;/h2&gt;

&lt;p&gt;The main reason to not gather user data is the risk of said data ending up
used for nefarious purposes. A simple case might be someone finding a password
that was incorrectly pasted in the wrong section of a config file, but more subtle
issues can arise like searching for users with old versions of ChefDK which have
a known vulnerability in their OpenSSL. To counter this we have two main lines of defense:
first is that all data is stored anonymously with a session identifier reset
every ten minutes. Second is that we plan to make sure all data is public from
the start, so we don’t have to wait for an inevitable leak to realize there is
a problem with the data.&lt;/p&gt;

&lt;p&gt;Anonymous data is never perfect though, and the rise in public data sets has lead
to new advances in de-anonymization. We will do our best to keep on top things,
but there will always be some passive risk. Therefore any data collection
must be optional with both per-user and per-project configuration, but more on that
below. Also it is worth restating here that only workstation commands are being
considered for this. &lt;code&gt;chef-client&lt;/code&gt;, &lt;code&gt;chef-solo&lt;/code&gt;, and the whole of Chef Server
do not and will not collect data of any kind.&lt;/p&gt;

&lt;p&gt;As a secondary thing, there is almost always a PR storm any time an open-source
project brings up this topic. I am probably contributing to this, though hopefully
with enough positive to outweigh any trolls I bring in to the discussion. Still,
this could end up being a harm to users if we have to fight FUD for a while, so
that should be considered. All planning and development of this feature is being
done in the open, which also hopefully helps offset the “sky is falling” crowd.&lt;/p&gt;

&lt;h2 id="opt-in-or-out"&gt;Opt In or Out&lt;/h2&gt;

&lt;p&gt;As mentioned above, all data collection will be 100% optional, but that brings
up the question of how to control it. We will need to pick a default yes/no, as
well as how to tell the user about this whole thing.&lt;/p&gt;

&lt;p&gt;Being silently disabled by default is effectively a non-option since we would get
so little data as to not be worth the effort to build the whole system, and
being silently enabled by default would (I think rightly) be viewed by many users
as a breach of trust and overstepping our bounds as a project.&lt;/p&gt;

&lt;p&gt;I think the best approach is a middle ground, off by default, but the first time
you run any instrumented command it asks if you want to allow telemetry with a
default of “yes” if you hammer on the enter key. This will mean we don’t get
data from TTY-less systems like CI servers, but is otherwise a balance between
user expectations and data quality. The opt-out will always be available afterwards
via a command like &lt;code&gt;chef telemetry disable&lt;/code&gt; or something, or you can manually
touch the &lt;code&gt;.chef/no_telemetry&lt;/code&gt; file (which can be checked in to source control to
permanently disable things for a project).&lt;/p&gt;

&lt;p&gt;We would love to &lt;a href="https://github.com/chef/chef-rfc/pull/269"&gt;hear from you&lt;/a&gt; if
you have thoughts on this though.&lt;/p&gt;

&lt;h2 id="prior-art"&gt;Prior Art&lt;/h2&gt;

&lt;p&gt;The proposed RFC covers this a bit but I wanted to dive in a bit more on some
ways other projects have handled this, mostly just to point out that this can
be done without the world ending.&lt;/p&gt;

&lt;p&gt;Both Chrome and Firefox (and probably Safari but who knows with them) gather
anonymized usages statistics as well as crash reports, and I don’t think I’ve
ever seen complaints about either. Firefox crash reports in particular, I know
several people that used to be on the team that managed that system and heard
from them frequently about how useful it was to the Firefox development team.&lt;/p&gt;

&lt;p&gt;Debian’s PopCon, Ubuntu’s Apport, and Fedora’s Retrace projects all gather some
level of usage information about packages (the latter two only on crashes) and
have been mostly uncontroversial in my experience (other than complaints about
apport from Python devs because it does weird stuff to the load path but that’s
neither here nor there). PopCon is mostly just for fun, but the error collection
has been cited as useful by a bunch of Ubuntu/Fedora projects.&lt;/p&gt;

&lt;p&gt;Homebrew added similar tracking via Google Analytics last year, and definitely
had some growing pains with it initially. After the initial flurry of fixes to
what was being collected, it seems to be going okay, though the Homebrew team
has been somewhat gruff in dealing with complaints which might indicate that they
get a lot of flak behind the scenes.&lt;/p&gt;

&lt;p&gt;Overall it seems like a lot of bigger projects add this kind of tracking to
little fanfare, especially desktop applications (like ChefDK).&lt;/p&gt;

&lt;h2 id="next-steps"&gt;Next Steps&lt;/h2&gt;

&lt;p&gt;Hopefully by now I’ve encouraged you to go &lt;a href="https://github.com/chef/chef-rfc/pull/269"&gt;drop us a line on the pull request&lt;/a&gt;.
We really do want to make sure we only move forward with this if it is in the
best interests of users and to do that we need to know what your interests are.
If anyone wants to provide feedback anonymously or has questions not answered here,
you can always reach me at &lt;a href="mailto:noah@coderanger.net"&gt;noah@coderanger.net&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>tag:coderanger.net,2017-02-13:/agents/</id>
    <title type="html">The Agents Are Coming</title>
    <published>2017-02-13T05:00:00Z</published>
    <updated>2017-02-13T18:11:01Z</updated>
    <link rel="alternate" href="https://coderanger.net/agents/"/>
    <content type="html">&lt;h2 id="no-not-hugo-weaving"&gt;No, not Hugo Weaving&lt;/h2&gt;

&lt;p&gt;Over the past five years, agent programs have been rising in both importance
and ubiquity. Apple mostly kicked things off for the modern generation of agents
with the release of Siri in 2011. Siri was a voice-based personal assistant,
able to send text messages, update calendars, and tell you the weather. From
this relatively-primitive nucleus, a whole industry has sprung up. While there
are several major archetypes of agents we’ll talk about below, they all share
a few major characteristics. The first is a generally conversational nature,
you talk to them either via voice or text but in a way you would talk to a human.
This is usually far from a perfect simulacrum, but Siri interactions are a far cry
from text adventure prompts or command line interfaces. The second unifying
feature is some concept of context. Again taking Siri as an example, it knows
where you are when you ask for the weather and so “does the right thing”. The
third trait is a bit more fuzzy, but most of the current agent developers have
tried to give their creations some kind of personality. All together, these
ideas bring together a broad swath of AI research and development and have fueled
an explosion in agent systems of all types.&lt;/p&gt;

&lt;h2 id="personal-voice-assistants"&gt;Personal Voice Assistants&lt;/h2&gt;

&lt;p&gt;While Siri is probably the most high profile, all of the tech titans have
thrown their hat in the ring of “personal voice assistants” in one way or
another. On the heels of Apple’s Siri has been Amazon’s Alexa (or Echo depending
on your wake-word). Google also has the somewhat less popular (and less-humanly
named) Google Assistant and Microsoft has Cortana, but Siri and Alexa have mostly
defined this battlefield with all others playing catch-up. A split has emerged
from those two, with Siri “living” in phones (and later, laptops) while Alexa
is based in mostly-unmoving physical objects in a home (Echo, Dot, etc).
Google has moved to offer both options for their Assistant, but this design
schism has certainly polarized much of the marketing of their respective camps,
if not the design of the software. While some third-party and open-source
teams have tried to offer additional options here (the &lt;a href="https://jasperproject.github.io/"&gt;Jasper&lt;/a&gt;
project probably being the most complete though I’m personally still hoping
&lt;a href="https://www.jibo.com/"&gt;Jibo&lt;/a&gt; manages to succeed), for the most part the big four
companies have this on lock-down. The value of a personal assistant is very
related to pervasive access and proximity, and each of the existing phone and
computer OS makers has made it clear they have no interest in allowing options
other than their own (for reasons that will become clear below).&lt;/p&gt;

&lt;h2 id="business-assistants"&gt;Business Assistants&lt;/h2&gt;

&lt;p&gt;With the personal assistant market mostly inaccessible, many companies wanting
to cash in on this space have turned to businesses as an untapped market. Rather
than integrating with just one person, these agents generally aim to fulfill the
same role as a secretary or office manager; scheduling meetings, sending out
team-wide updates, and other relatively simple-but-frequently-annoying duties.
The two main players I see a lot in this space are Clara from &lt;a href="https://claralabs.com/"&gt;Clara Labs&lt;/a&gt;
and Amy from &lt;a href="https://x.ai/"&gt;X.ai&lt;/a&gt; (and yes, feminine names/traits for
subservient agents is apparently just a given at this point), but they are
proliferating as fast as VCs can write checks it seems. My guess is that travel
management will be next on the list for many of these companies, but time will
tell. Unfortunately one big problem with a lot of these agents is the open
secret that much of their AI is heavily “supervised” by human “trainers”. The
labor politics of AI and mechanization are complex to say the least, but further
development here is certainly likely to have an impact on the polarization of
tech industry workforces, with more of the few remaining entry level jobs being
moved into some mix of agent AI and “virtual remote assistant” services.&lt;/p&gt;

&lt;h2 id="chat-bots"&gt;Chat Bots&lt;/h2&gt;

&lt;p&gt;Focused more on consumer-facing systems, chat bots have exploded even more recently
than the other two. The first wave was the now-ubiquitous “customer support chat”
windows on websites. While I’m sure some of these originally used human reps just
as they would in a call-center, more and more are moving to either “AI assisted”
or straight-up agent AI systems. Nina from Nuance (side note: Nuance has been
involved in almost every company I’ve listed here in one way other another, they
are probably one of the most important AI companies you’ve never heard of), is
one of the most visible but almost every customer support firm has added agent
tools to their offering.&lt;/p&gt;

&lt;p&gt;More recently we’ve seen several major chat systems build frameworks for
companies to interact with customers/users via chat bots. Facebook has been
among the most visible, promoting all manner of companies as bot partners.
Slack has gone even further, not just promoting bot makers but building a
custom venture fund to try and attract people towards integrating with Slack.
These kinds of chat bots are definitely the least agent-y of what we’ve seen,
but it seems clear that there will be some convergent evolution as things move
forward. Rather than acting on the behalf of the user, the bots are projections
of a company or service, but beyond that the interaction structure is very
similar.&lt;/p&gt;

&lt;h2 id="the-near-future"&gt;The Near Future&lt;/h2&gt;

&lt;p&gt;Where are we going from here? I think there are three main areas we are going
to see growth in over the short term. The first is improvements in the
contextual awareness of agents. The simplest example is that no current voice
agent can distinguish multiple voices. The uses for this kind of data would be
huge, like saying “play my favorite song” to Alexa or having Siri not pick up
on &lt;a href="http://www.theverge.com/2017/1/7/14200210/amazon-alexa-tech-news-anchor-order-dollhouse"&gt;commands from a TV show&lt;/a&gt;.
More context into our lives will allow more reactive
behavior, potentially even moving into proactive systems like knowing to pause
Spotify on my phone and start it on my laptop or Echo when I walk into my house.&lt;/p&gt;

&lt;p&gt;Next, I think we’ll see a major increase in the reach of agents. Right now Siri
can turn my lights on and off, and set calendar items or reminders, but not
really a whole lot of real “weight”. Alexa’s shopping integration offers a
broader grasp, but we are still a long way from connecting an agent to my
Paypal account, or my (hopefully soon to be self-driving) car, or even my email.
The more systems an agent is hooked in to, the more potential to cause problems.
It will take a lot of time and slow build-up, but seems inevitable that reach
will grow over time.&lt;/p&gt;

&lt;p&gt;Right now all of these agents really only “exist” while you’re interacting with
them. They have no permanent existence, and so can’t take action outside of a
specific call-and-response prompt. Of all the limitations on current agent tools,
this seems this most restrictive. There is no way to tell my Siri to talk to
someone else’s Siri to schedule a meeting, or for me to authorize my Alexa to
let someone else stream to my TV. The first inklings of this are starting to
appear with push notifications in the same way as for phone apps, but this is
only the beginning. For a possible end-game view I turn to a story from Vint Cerf
during a talk he gave on the future of the internet in 2015. He talked about how
he has security cameras and motion detectors throughout his home already, something
which requires the utmost in information security for hopefully obvious reasons.
But if there was a fire, would you want the fire department to get access to that
data? If they could immediately know where everyone in the house is and what
situation they are in, it is a virtual certainty that lives will be saved. At
the same time no reasonable (I think I can say this with full bipartisan agreement)
person wants to give a government agency a live video feed of their home. So
who or what should make the call to enable or disable the data sharing? This is
a role that a more autonomous agent system could fill, though only time will
tell if we ever get this far.&lt;/p&gt;

&lt;p&gt;Beyond the technical improvements, I think the biggest story for agents in the
coming years will be their commercialization. Telegram already refers to their
agent directory as a “store” (though for now I think all of them are gratis).
Apple has recently moved to allow limited third-party development in Siri, and
Amazon has been pushing hard on their Alexa APIs. Just as phone apps led to the
gold rush that was the iOS and Android app stores, I think we will see a major
push towards app-ification of agents or agent functionality/integrations.&lt;/p&gt;

&lt;h2 id="the-further-future"&gt;The Further Future?&lt;/h2&gt;

&lt;p&gt;Too move even further into speculation tinged by sci-fi, I think (and hope)
there is a possibility of agent systems becoming more of a proxy of ourselves
in digital space. We’re already seeing lots of growth in tools for “attention
modeling” (eg. Facebook timeline) to help manage the massive influx of information
that is part of modern life. I think over time this might fuse with the other
functions mentioned above to form a more complete virtual projection. This also
harkens back to the view of personal agents as replacing a a human assistant or
secretary, but as models of attention and trust get better these avatars can
accomplish more by interacting with each other instead of humans. If I ask my
agent to find a good time for dinner with a friend, it can just do that rather
than the “well what time is good for you” dance. These kinds of avatars have
been such a common trope in science fiction that it is hard to even imagine
them without taking fairly direct cues from specific stories or instances, but
as both our rate of data ingestion and interconnectedness show no sign of slowing,
something along these lines seems inevitable.&lt;/p&gt;

&lt;p&gt;With that said, a lot will have to change in our social and legal landscape to
make this truly viable. Already today we’re beginning to see some backlash
against poorly secured hosted services. While Siri has mostly been limited to
living in a specific phone, Alexa and Google Assistant are built from the
ground up as hosted services like GMail or Amazon Video. These kinds of services
have already firmly blurred the line between expectations of privacy versus
what is actually a personal space, and things are only likely to get worse.
Giving an agent access to all your personal information, habits, and data to
allow it to better help you is going to be a powerful motivator but with things
like the iCloud photo thefts or NSL data disclosures still fresh in many minds
(okay, maybe more the former than the latter but I can dream) I hope to see
a push for legal protections for this kind of system or for hosted/shared services
more broadly as they do truly become an extension of ourselves.&lt;/p&gt;

&lt;h2 id="why"&gt;Why?&lt;/h2&gt;

&lt;p&gt;So why has all of the this been happening? Why do I think this field is important
enough to spend the time to write several thousand words on it? Because if done
well I think agent systems will open up whole new worlds of interaction in the
same way that the jump from teletype command line interfaces to GUIs and mice
did for workstations, or that touchscreens did for phones. We are fundamentally
social creatures, and interacting with the world through language is much more
intuitive than the forced abstractions we’ve created in the computing world like
menu bars and double clicking. Agents, and especially voice agents offer a
way to streamline the daily lives of an almost uncountable number of people
around the world.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>tag:coderanger.net,2016-11-29:/todo-list/</id>
    <title type="html">My Todo List</title>
    <published>2016-11-29T05:00:00Z</published>
    <updated>2016-12-01T19:32:25Z</updated>
    <link rel="alternate" href="https://coderanger.net/todo-list/"/>
    <content type="html">&lt;p&gt;With the US Thanksgiving holiday this past week, I decided to take some time and
write down all the projects that are still on my overall todo list. To lead off
with a disclaimer: this is not a promise I’ll ever get to any of these or that
they will happen in any particular order. This is mostly for my own benefit but
hopefully it provides some value to others, if only documenting what still needs
fixing. Also this is just my own list and does not reflect the opinions of Chef
as a project or Chef Software as a company, and is neither in a specific order
nor exhaustive.&lt;/p&gt;

&lt;p&gt;If any or all of these projects sound interesting to you and you would like to
see them happen sooner, maybe consider checking out my &lt;a href="/hire-me/"&gt;job search page&lt;/a&gt;?&lt;/p&gt;

&lt;h4 id="finish-porting-foodcritic-rules-to-rubocop-chef"&gt;Finish Porting Foodcritic Rules to Rubocop-Chef&lt;/h4&gt;

&lt;p&gt;This is in-progress now but still has a ways to go. You can see the current
status in the &lt;a href="https://github.com/poise/rubocop-chef/"&gt;rubocop-chef README&lt;/a&gt;.
The goal is to replace as much of Foodcritic as possible with Rubocop so that
we can get features like multiple message levels, autocorrect, and a unified
configuration file. It will eventually be integrated with the &lt;code&gt;cookstyle&lt;/code&gt; project
as a one-stop-shop for opinions on how to write Chef cookbooks if you want to
use an existing style guide.&lt;/p&gt;

&lt;h4 id="secrets-management-abstractionapi"&gt;Secrets Management Abstraction/API&lt;/h4&gt;

&lt;p&gt;I’ve talked about this a bunch and it’s very close to the top of my list right
now. Basically I want to build a modular API for accessing &lt;a href="/talks/secrets/"&gt;secrets&lt;/a&gt;
from Chef code so that cookbooks can be written to use “any” storage system (chef-vault,
Hashicorp Vault, S3/KMS, etc). The DSL elements will probably look very similar
to my &lt;a href="https://github.com/poise/citadel/"&gt;&lt;code&gt;citadel&lt;/code&gt;&lt;/a&gt; cookbook but with an internal
configuration system integrated with Chef and plugable backends.&lt;/p&gt;

&lt;h4 id="deeper-integration-between-chef-and-hashivault"&gt;Deeper Integration Between Chef and HashiVault&lt;/h4&gt;

&lt;p&gt;I wrote up &lt;a href="/chef-and-vault/"&gt;a lot of words about this&lt;/a&gt;, but it is still vaporware.
Would be nice to spend some cycles making at least some parts of this exist.&lt;/p&gt;

&lt;h4 id="ram-usage-analysis-for-"&gt;RAM Usage Analysis for &lt;code&gt;poise-profiler&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This has been sitting half-finished for a while now and needs to get over the
finish line. &lt;a href="https://github.com/poise/poise-profiler/"&gt;&lt;code&gt;poise-profiler&lt;/code&gt;&lt;/a&gt;
already provides a lot of timing data for Chef performance, I want to add memory
usage to that as well to see when a recipe or resource is allocating a large
amount of stuff (usually search results) and then not using it.&lt;/p&gt;

&lt;h4 id="meta-helper-library"&gt;Meta-Helper Library&lt;/h4&gt;

&lt;p&gt;A common stumbling block for new and experienced Chef users alike is adding
helper methods to Chef. Many of the old patterns in the ecosystem (like &lt;code&gt;Chef::Recipe.send(:include, ...)&lt;/code&gt;)
are both dangerous and unnecessary. A library cookbook to provide an API to
declare various kinds of helper methods could both simplify the process and
help document the tradeoffs of the different kinds of helpers.&lt;/p&gt;

&lt;h4 id="implement-rfc-33"&gt;Implement RFC 33&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/chef/chef-rfc/blob/master/rfc033-root-aliases.md"&gt;Chef RFC 33&lt;/a&gt;
adds support for a simplified folder structure for cookbooks with the aim of
making small wrapper cookbooks or other short one-offs feel less boiler-plate-y.
I implemented this long ago, but the branch bit-rotted before we could agree on
the RFC so it needs to be started from scratch by now.&lt;/p&gt;

&lt;h4 id="implement-rfc-36"&gt;Implement RFC 36&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/chef/chef-rfc/blob/master/rfc036-dialects.md"&gt;Chef RFC 36&lt;/a&gt;
adds support for writing Chef recipes and some other Chef-related files in
languages other than Ruby (or JSON). Specifically I want to add things like
YAML and TOML support for attributes files and maybe some day Python/JavaScript
support for recipe code. Similar to 33, there are a few old implementation of
this but any new attempt will have to start over.&lt;/p&gt;

&lt;h4 id="implement-rfc-75"&gt;Implement RFC 75&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/chef/chef-rfc/blob/master/rfc075-multi-policy.md"&gt;Chef RFC 75&lt;/a&gt;
adds support for assigning more than one policy to a node to allow use of the
Policyfile workflow in some situations where having a single compiled object is
problematic. I’ve now tried implementing this twice, only to find the approach
I used was fatally flawed, but I still think the feature is important and
should exist.&lt;/p&gt;

&lt;h4 id="improve-test-kitchens-concurrent-mode"&gt;Improve Test-Kitchen’s Concurrent Mode&lt;/h4&gt;

&lt;p&gt;With the surge in Chef releases thanks to the new &lt;a href="https://github.com/chef/chef-rfc/blob/master/rfc081-release-cadence.md"&gt;monthly cadence&lt;/a&gt;,
CI build times have been rising rapidly. For most of my cookbooks I currently
run four test platforms across 18 Chef versions, and Travis only allows for
5 of those to run concurrently at most. Test Kitchen does support its own
concurrent execution mode to speed things up, but currently the output ends up
intermixed between the concurrent runs making it almost unreadable. For
interactive use this can be okay because you just repeat a failed command to
see the error message, but in a CI system this isn’t an option. An improved
system could buffer the Test Kitchen output to files and then display it all
together when an instance completes the command. Not a ton of work, but anything
involving threads (which TK’s concurrent mode is built on) is always deceptively
difficult.&lt;/p&gt;

&lt;h4 id="use--for-kitchen-docker"&gt;Use &lt;code&gt;docker exec&lt;/code&gt; for Kitchen-Docker&lt;/h4&gt;

&lt;p&gt;Currently the &lt;code&gt;kitchen-docker&lt;/code&gt; plugin launches sshd inside the container and
then uses the normal Test Kitchen SSH machinery to connect to the new instance.
This works okay, but it requires a fair bit of complexity around authentication
and &lt;code&gt;sudo&lt;/code&gt; access that can sometimes make testing more difficult. In the years
since the plugin was first written, Docker has added their own remote
execution system in form of &lt;code&gt;docker exec&lt;/code&gt;. Switching to this would allow for fewer
moving pieces and simplify the default &lt;code&gt;Dockerfile&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id="improve-kitchen-docker-build-performance"&gt;Improve Kitchen-Docker Build Performance&lt;/h4&gt;

&lt;p&gt;The default &lt;code&gt;Dockerfile&lt;/code&gt; used by &lt;code&gt;kitchen-docker&lt;/code&gt; is unfortunately slow to
build sometimes. Much of this is due to the creation of many intermediary
layers which could be collapsed together. I’ve already done a lot of this for
&lt;a href="https://github.com/poise/poise-boiler/blob/master/lib/poise_boiler/helpers/kitchen/Dockerfile.erb"&gt;my own use&lt;/a&gt;
but it should be ported back out to the rest of the community.&lt;/p&gt;

&lt;h4 id="new-cookbook-for-supervisord"&gt;New Cookbook for Supervisord&lt;/h4&gt;

&lt;p&gt;The current &lt;code&gt;supervisord&lt;/code&gt; cookbook is in need of a major upgrade and retrofit. This can
take advantage of the improved Python resources from &lt;code&gt;poise-python&lt;/code&gt; and the
patterns built as part of the new &lt;code&gt;poise-monit&lt;/code&gt; cookbook. This would also include
adding &lt;code&gt;supervisord&lt;/code&gt; support to &lt;code&gt;poise-service&lt;/code&gt;, allowing it to be used with
any cookbook based around that service abstraction.&lt;/p&gt;

&lt;h4 id="resource-based-java-cookbook"&gt;Resource-based Java Cookbook&lt;/h4&gt;

&lt;p&gt;The current Java cookbook has been a source of much frustration in the Chef
world. I’ve already got a lot of tools built for managing language runtimes
with Chef as part of the &lt;code&gt;poise-python/ruby/javascript&lt;/code&gt; work, though undoubtedly
Java will bring its own unique complications.&lt;/p&gt;

&lt;h4 id="more-providers-for-python-and-ruby"&gt;More Providers for Python and Ruby&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;poise-python&lt;/code&gt; and &lt;code&gt;poise-ruby&lt;/code&gt; cookbooks support multiple providers for
how to install their respective language runtimes, but they could both stand
to add some more options. On the Python side, source installs still need to be
added (refactored from the existing source install support for Ruby) and some
common PPAs like Deadsnakes should be included. For Ruby, the Brightbox Ruby
packages would be a good option for many.&lt;/p&gt;

&lt;h4 id="improved-scl-handling-on-rhel"&gt;Improved SCL Handling on RHEL&lt;/h4&gt;

&lt;p&gt;The recent-ish 2.0 release of &lt;code&gt;poise-langauages&lt;/code&gt; fixed support for Software
Collections on CentOS, but unfortunately broke many RHEL users. It follows the
official RedHat instructions to configure the RHSCL repositories through RedHat’s
subscription system, but this flat out doesn’t work for most cloud images and
development systems. Cross-installing the CentOS SCLs on RHEL seems like a reasonable
fix, as well as a more flexible &lt;code&gt;:rhscl&lt;/code&gt; provider that works on at least some
more of the ways RHEL can be run.&lt;/p&gt;

&lt;h4 id="finish-refactoring-halite-spec-helpers"&gt;Finish Refactoring Halite Spec Helpers&lt;/h4&gt;

&lt;p&gt;As part of the Halite project, I built out a lot of helper methods for writing
Chef unit tests for complex cookbooks. Most of these are not specifically tied
to Halite so I’ve started splitting them out to a new gem: &lt;a href="https://github.com/poise/poise-spec"&gt;&lt;code&gt;poise-spec&lt;/code&gt;&lt;/a&gt;.
These rest of the helpers need to be split out and then Halite’s helper module
should be rewritten to use and extend &lt;code&gt;poise-spec&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id="migrate-the-chef-documentation"&gt;Migrate the Chef Documentation&lt;/h4&gt;

&lt;p&gt;Now that we’ve started to clean up the Sphinx documentation for Chef, I want to
move the docs for Chef itself into Chef’s code repository. This will make it
easier to require Chef changes to include doc updates in the same way as we
currently require tests, as well as making it easier to version the docs alongside
Chef. Some complexity will be needed to figure out how to integrate the Chef
docs with the rest of the Chef’s product documentation so it can all be published
to &lt;code&gt;docs.chef.io&lt;/code&gt; but I’m sure we can work through that.&lt;/p&gt;

&lt;h4 id="integrate-yard-and-sphinx"&gt;Integrate YARD and Sphinx&lt;/h4&gt;

&lt;p&gt;The current Chef docs are written using an excellent framework called Sphinx.
It has lots of tools and helpers for writing high-quality narrative documentation,
but most of it’s reference documentation tools are aimed at the Python and C
worlds where Sphinx comes from originally. The gold standard for auto-extracted
code/reference documentation in the Ruby world is YARD, and we’ve already begun
adding more YARD comment markup throughout Chef and the broader ecosystem. The
missing link between them is a way to integrate YARD data into Sphinx docs
directly. A related project here is using YARD data to generate cookbook
READMEs like other Sphinx documents.&lt;/p&gt;

&lt;h4 id="rewrite-poise-using-new-apis"&gt;Rewrite Poise Using New APIs&lt;/h4&gt;

&lt;p&gt;My &lt;a href="https://github.com/poise/poise/"&gt;Poise helper library&lt;/a&gt; has aged reasonably
but enough has changed in Chef core to make a major upgrade look more and more
inviting. This will probably not happen until ~April 2017 (i.e. the Chef 13.0
release), when it will be more reasonable to drop support for Chef &amp;lt;12.5.
Notably the new resource property APIs will allow major refactoring of Poise’s
helper methods and a lot fewer uses of &lt;code&gt;define_method&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id="other-stuff"&gt;Other Stuff&lt;/h4&gt;

&lt;p&gt;Outside of the Chef world I’ve got a few projects kicking around in my brain.
I’m a strong believer that Kubernetes is going to win the container orchestration
wars, but it currently has pretty much nothing for testing and static analysis.
Ditto on Terraform for infrastructure management (though there is a
&lt;code&gt;kitchen-terraform&lt;/code&gt; plugin that looks very interesting for the future). Someone bringing
the same “teaching ops folks how to software” approach I’ve aimed at the Chef community
is probably going to be needed in the long run.&lt;/p&gt;

&lt;h3 id="tldr"&gt;tl;dr&lt;/h3&gt;

&lt;p&gt;I’ve got a big backlog of projects in the Chef world (and beyond) and if you want to help
make them happen you should &lt;a href="/hire-me/"&gt;hire me&lt;/a&gt;.&lt;/p&gt;</content>
  </entry>
</feed>
