<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bcreane.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://bcreane.github.io//" rel="alternate" type="text/html" /><updated>2025-11-19T17:05:21+00:00</updated><id>https://bcreane.github.io//feed.xml</id><title type="html">Brendan Creane – Dev Notes</title><subtitle>Dev notes.</subtitle><entry><title type="html">Blocking until a kubernetes pod is ready - golang edition</title><link href="https://bcreane.github.io//kubernetes/2018/05/10/golang-k8s-pod-ready.html" rel="alternate" type="text/html" title="Blocking until a kubernetes pod is ready - golang edition" /><published>2018-05-10T19:18:11+00:00</published><updated>2018-05-10T19:18:11+00:00</updated><id>https://bcreane.github.io//kubernetes/2018/05/10/golang-k8s-pod-ready</id><content type="html" xml:base="https://bcreane.github.io//kubernetes/2018/05/10/golang-k8s-pod-ready.html"><![CDATA[<h1 id="tldr">TL;DR</h1>

<p>While building a simple install script for <a href="https://www.tigera.io/cnx/">Tigera’s CNX</a>
product, I received equally emphatic advice to use <a href="https://www.gnu.org/software/bash/">bash</a>
as well as <a href="https://golang.org/">golang</a>.</p>

<p>I decided to build the same (subset of) functionality twice: once in
<a href="https://bcreane.github.io/kubernetes/2018/04/07/bash-k8s-pod-ready.html">bash</a>
and once again in <a href="https://github.com/bcreane/k8sutils/blob/master/utils.go">golang</a>.</p>

<h2 id="outcome">Outcome</h2>

<ul>
  <li>The bash function took less than half the time to write and it appears equally robust and not particularly hard to read.</li>
  <li>The golang executable is better for scale: unit tests, strongly typed variables, scoped functionality, and more make
it easier to maintain and extend safely.</li>
</ul>

<h1 id="wait-till-pod-is-running---golang-edition">Wait till pod is running - golang edition</h1>

<p>My package builds upon the
<a href="https://github.com/kubernetes/kubernetes/blob/master/test/e2e/framework/util.go">kubernetes e2e utils framework</a>.
This robust framework monitors the state of a kubernetes cluster using
<a href="https://github.com/kubernetes/client-go">client-go</a>.</p>

<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">package</span> <span class="n">k8sutils</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"k8s.io/api/core/v1"</span>
	<span class="n">meta_v1</span> <span class="s">"k8s.io/apimachinery/pkg/apis/meta/v1"</span>
	<span class="s">"k8s.io/apimachinery/pkg/util/wait"</span>
	<span class="s">"k8s.io/client-go/kubernetes"</span>
	<span class="s">"k8s.io/kubernetes/pkg/client/conditions"</span>
	<span class="s">"time"</span>
<span class="p">)</span>

<span class="c">// return a condition function that indicates whether the given pod is</span>
<span class="c">// currently running</span>
<span class="k">func</span> <span class="n">isPodRunning</span><span class="p">(</span><span class="n">c</span> <span class="n">kubernetes</span><span class="o">.</span><span class="n">Interface</span><span class="p">,</span> <span class="n">podName</span><span class="p">,</span> <span class="n">namespace</span> <span class="kt">string</span><span class="p">)</span> <span class="n">wait</span><span class="o">.</span><span class="n">ConditionFunc</span> <span class="p">{</span>
	<span class="k">return</span> <span class="k">func</span><span class="p">()</span> <span class="p">(</span><span class="kt">bool</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"."</span><span class="p">)</span> <span class="c">// progress bar!</span>

		<span class="n">pod</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">CoreV1</span><span class="p">()</span><span class="o">.</span><span class="n">Pods</span><span class="p">(</span><span class="n">namespace</span><span class="p">)</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="n">podName</span><span class="p">,</span> <span class="n">meta_v1</span><span class="o">.</span><span class="n">GetOptions</span><span class="p">{</span><span class="n">IncludeUninitialized</span><span class="o">:</span> <span class="no">true</span><span class="p">})</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">return</span> <span class="no">false</span><span class="p">,</span> <span class="n">err</span>
		<span class="p">}</span>

		<span class="k">switch</span> <span class="n">pod</span><span class="o">.</span><span class="n">Status</span><span class="o">.</span><span class="n">Phase</span> <span class="p">{</span>
		<span class="k">case</span> <span class="n">v1</span><span class="o">.</span><span class="n">PodRunning</span><span class="o">:</span>
			<span class="k">return</span> <span class="no">true</span><span class="p">,</span> <span class="no">nil</span>
		<span class="k">case</span> <span class="n">v1</span><span class="o">.</span><span class="n">PodFailed</span><span class="p">,</span> <span class="n">v1</span><span class="o">.</span><span class="n">PodSucceeded</span><span class="o">:</span>
			<span class="k">return</span> <span class="no">false</span><span class="p">,</span> <span class="n">conditions</span><span class="o">.</span><span class="n">ErrPodCompleted</span>
		<span class="p">}</span>
		<span class="k">return</span> <span class="no">false</span><span class="p">,</span> <span class="no">nil</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="c">// Poll up to timeout seconds for pod to enter running state.</span>
<span class="c">// Returns an error if the pod never enters the running state.</span>
<span class="k">func</span> <span class="n">waitForPodRunning</span><span class="p">(</span><span class="n">c</span> <span class="n">kubernetes</span><span class="o">.</span><span class="n">Interface</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">podName</span> <span class="kt">string</span><span class="p">,</span> <span class="n">timeout</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">wait</span><span class="o">.</span><span class="n">PollImmediate</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">,</span> <span class="n">timeout</span><span class="p">,</span> <span class="n">isPodRunning</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">podName</span><span class="p">,</span> <span class="n">namespace</span><span class="p">))</span>
<span class="p">}</span>

<span class="c">// Returns the list of currently scheduled or running pods in `namespace` with the given selector</span>
<span class="k">func</span> <span class="n">ListPods</span><span class="p">(</span><span class="n">c</span> <span class="n">kubernetes</span><span class="o">.</span><span class="n">Interface</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">selector</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="o">*</span><span class="n">v1</span><span class="o">.</span><span class="n">PodList</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="n">listOptions</span> <span class="o">:=</span> <span class="n">meta_v1</span><span class="o">.</span><span class="n">ListOptions</span><span class="p">{</span><span class="n">IncludeUninitialized</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span> <span class="n">LabelSelector</span><span class="o">:</span> <span class="n">selector</span><span class="p">}</span>
	<span class="n">podList</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">c</span><span class="o">.</span><span class="n">CoreV1</span><span class="p">()</span><span class="o">.</span><span class="n">Pods</span><span class="p">(</span><span class="n">namespace</span><span class="p">)</span><span class="o">.</span><span class="n">List</span><span class="p">(</span><span class="n">listOptions</span><span class="p">)</span>

	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">err</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">podList</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>

<span class="c">// Wait up to timeout seconds for all pods in 'namespace' with given 'selector' to enter running state.</span>
<span class="c">// Returns an error if no pods are found or not all discovered pods enter running state.</span>
<span class="k">func</span> <span class="n">WaitForPodBySelectorRunning</span><span class="p">(</span><span class="n">c</span> <span class="n">kubernetes</span><span class="o">.</span><span class="n">Interface</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">selector</span> <span class="kt">string</span><span class="p">,</span> <span class="n">timeout</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
	<span class="n">podList</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">ListPods</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">selector</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">err</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">podList</span><span class="o">.</span><span class="n">Items</span><span class="p">)</span> <span class="o">==</span> <span class="m">0</span> <span class="p">{</span>
		<span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"no pods in %s with selector %s"</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">selector</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">pod</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">podList</span><span class="o">.</span><span class="n">Items</span> <span class="p">{</span>
		<span class="k">if</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">waitForPodRunning</span><span class="p">(</span><span class="n">c</span><span class="p">,</span> <span class="n">namespace</span><span class="p">,</span> <span class="n">pod</span><span class="o">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="n">timeout</span><span class="p">)</span><span class="o">*</span><span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">);</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
			<span class="k">return</span> <span class="n">err</span>
		<span class="p">}</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In addition to the <code class="language-plaintext highlighter-rouge">k8sutils</code> package above, I built a simple <a href="https://github.com/bcreane/k8sutils/blob/master/watch/watch.go"><code class="language-plaintext highlighter-rouge">main</code></a>
executable package:</p>

<div class="language-golang highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"flag"</span>
	<span class="s">"fmt"</span>
	<span class="s">"github.com/bcreane/k8sutils"</span>
	<span class="n">log</span> <span class="s">"github.com/sirupsen/logrus"</span>
	<span class="s">"k8s.io/client-go/kubernetes"</span>
	<span class="s">"k8s.io/client-go/tools/clientcmd"</span>
	<span class="s">"k8s.io/client-go/util/homedir"</span>
	<span class="s">"os"</span>
	<span class="s">"path/filepath"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">var</span> <span class="n">kubeConfig</span> <span class="o">*</span><span class="kt">string</span>
	<span class="k">if</span> <span class="n">home</span> <span class="o">:=</span> <span class="n">homedir</span><span class="o">.</span><span class="n">HomeDir</span><span class="p">();</span> <span class="n">home</span> <span class="o">!=</span> <span class="s">""</span> <span class="p">{</span>
		<span class="n">kubeConfig</span> <span class="o">=</span> <span class="n">flag</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="s">"kubeconfig"</span><span class="p">,</span> <span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">home</span><span class="p">,</span> <span class="s">".kube"</span><span class="p">,</span> <span class="s">"config"</span><span class="p">),</span> <span class="s">"(optional) absolute path to the kubeconfig file"</span><span class="p">)</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">kubeConfig</span> <span class="o">=</span> <span class="n">flag</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="s">"kubeconfig"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="s">"absolute path to the kubeconfig file"</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="k">var</span> <span class="n">namespace</span> <span class="o">=</span> <span class="n">flag</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="s">"namespace"</span><span class="p">,</span> <span class="s">"default"</span><span class="p">,</span> <span class="s">"namespace"</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">selector</span> <span class="o">=</span> <span class="n">flag</span><span class="o">.</span><span class="n">String</span><span class="p">(</span><span class="s">"selector"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="s">"pod selector"</span><span class="p">)</span>
	<span class="k">var</span> <span class="n">timeout</span> <span class="o">=</span> <span class="n">flag</span><span class="o">.</span><span class="n">Int</span><span class="p">(</span><span class="s">"timeout"</span><span class="p">,</span> <span class="m">30</span><span class="p">,</span> <span class="s">"timeout in seconds"</span><span class="p">)</span>
	<span class="n">flag</span><span class="o">.</span><span class="n">Parse</span><span class="p">()</span>

	<span class="n">config</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">clientcmd</span><span class="o">.</span><span class="n">BuildConfigFromFlags</span><span class="p">(</span><span class="s">""</span><span class="p">,</span> <span class="o">*</span><span class="n">kubeConfig</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">clientSet</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">kubernetes</span><span class="o">.</span><span class="n">NewForConfig</span><span class="p">(</span><span class="n">config</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

	<span class="c">// Block up to timeout seconds for listed pods in namespace/selector to enter running state</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">k8sutils</span><span class="o">.</span><span class="n">WaitForPodBySelectorRunning</span><span class="p">(</span><span class="n">clientSet</span><span class="p">,</span> <span class="o">*</span><span class="n">namespace</span><span class="p">,</span> <span class="o">*</span><span class="n">selector</span><span class="p">,</span> <span class="o">*</span><span class="n">timeout</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">The pod never entered running phase</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>
		<span class="n">os</span><span class="o">.</span><span class="n">Exit</span><span class="p">(</span><span class="m">1</span><span class="p">)</span>
	<span class="p">}</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">All pods in namespace=</span><span class="se">\"</span><span class="s">%s</span><span class="se">\"</span><span class="s"> with selector=</span><span class="se">\"</span><span class="s">%s</span><span class="se">\"</span><span class="s"> are running!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">*</span><span class="n">namespace</span><span class="p">,</span> <span class="o">*</span><span class="n">selector</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Invoking the program is simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c"># Wait up to 30 seconds for pods w/ selector and namespace to enter running phase</span>
    ./watch <span class="nt">--selector</span><span class="o">=</span>k8s-app<span class="o">=</span>kube-dns <span class="nt">--namespace</span><span class="o">=</span>kube-system <span class="nt">--timeout</span><span class="o">=</span>30<span class="sb">`</span>
</code></pre></div></div>

<p>This works about the same as the bash script I wrote a few weeks ago.</p>

<h1 id="take-away">Take away</h1>

<ul>
  <li>Bash is quicker to write and often more condensed than golang since it builds on
powerful utilities such as <code class="language-plaintext highlighter-rouge">kubectl</code>.</li>
  <li>golang is more maintainable and robust, and frankly cooler.</li>
</ul>

<p>If you need a simple script that’s less than a few hundred lines, consider bash.
Otherwise golang’s higher upfront cost but much more robust extensibility and
verifiability are a better choice.</p>

<p>The real install script has grown to about 1,000 lines of bash. Don’t forget to allow
head room for feature creep - your 50 line bash script may grow into 500 lines before
you know it!</p>]]></content><author><name></name></author><category term="kubernetes" /><summary type="html"><![CDATA[TL;DR]]></summary></entry><entry><title type="html">Blocking until a kubernetes pod is ready - bash edition</title><link href="https://bcreane.github.io//kubernetes/2018/04/07/bash-k8s-pod-ready.html" rel="alternate" type="text/html" title="Blocking until a kubernetes pod is ready - bash edition" /><published>2018-04-07T09:41:11+00:00</published><updated>2018-04-07T09:41:11+00:00</updated><id>https://bcreane.github.io//kubernetes/2018/04/07/bash-k8s-pod-ready</id><content type="html" xml:base="https://bcreane.github.io//kubernetes/2018/04/07/bash-k8s-pod-ready.html"><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>Bash is rightly critized as a train wreck when it comes to writing readable,
maintainable scripts. See <a href="http://databio.org/posts/shell_scripts.html">Stop writing shell Scripts!</a>
for a sample.</p>

<p>If you’re careful, it’s possible to write a bash script that is
sufficiently well-written to (potentially) justify the extra maintenance costs.</p>

<p>I’ll solve a useful problem in both bash as well as golang, comparing up-front the costs.</p>

<p>First edition: bash script that blocks until a kubernetes pod is “ready,”
or a timeout fires.</p>

<h1 id="some-bash-best-practices">Some bash best practices</h1>

<p>We’ll use a few features of “modern” bash (if that’s not too much of an oyxmoron):</p>

<ul>
  <li>
    <p>Subshells: rather than using backticks, isolating a command in a subshell
is good security practice:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  status=$(ls)  # grab the output from a subshell
</code></pre></div>    </div>
  </li>
  <li>
    <p>Arithmetic:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  $((count--))  # looks like C if you squint hard enough
</code></pre></div>    </div>
  </li>
  <li>
    <p>String regexes: bash supports some python-esque operators which are concise
and convenient, though you must use the clumsy <code class="language-plaintext highlighter-rouge">[[ ]]</code> test operator:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    if [[ $(echo "hello, fool") =~ "fool" ]]; then  # search for "fool" substring
      echo You are indeed a fool.
    fi
</code></pre></div>    </div>
  </li>
  <li>
    <p>Use <code class="language-plaintext highlighter-rouge">local</code> to reduce the scope of variables, e.g.:</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function fun() {
  local rabbit="marty"  # $rabbit is not visible outside the function "fun()"
  echo "${rabbit}"
}

fun             # invoke function "fun()", prints "marty"
echo "$rabbit"  # in the global scope, "$rabbit" is uninitialized, prints ""
</code></pre></div>    </div>
  </li>
</ul>

<h1 id="bash-is-glue">Bash is glue</h1>

<p>Obviously bash is just the glue that binds a myriad of tools together - similar
to the range of packages available to a golang program.</p>

<p>In this case, we’ll use the following standalone programs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kubectl</code>: reports the pod’s state, optionally in json format</li>
  <li><code class="language-plaintext highlighter-rouge">jq</code>: parses and filters <code class="language-plaintext highlighter-rouge">kubectl</code>’s json output</li>
</ul>

<h1 id="bash-script-to-block-until-a-pod-is-ready-or-timeout">Bash script to block until a pod is ready (or timeout)</h1>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash
# Brendan Creane
# Block until a pod specified by a selector is "ready"
# or a timeout occurs.

#
# podStatus() - takes a selector as argument, e.g. "k8s-app=kube-dns"
# and returns the pod "ready" status as a bool string ("true"). Note that if
# the pod is in the "pending" state, there is no containerStatus yet, so
# podStatus() returns an empty string. Success means seeing the "true"
# substring, but failure can be "false" or an empty string.
#
function podStatus() {
  local label="$1"
  local status=$(kubectl get pods --selector="${label}" -o json --all-namespaces | jq -r '.items[] | .status.containerStatuses[]? | [.name, .image, .ready|tostring] |join(":")')
  echo "${status}"
}

#
# blockUntilPodIsReady() - takes a pod selector and a timeout in seconds
# as arguements. If the pod never stabilizes, bail. Otherwise return as
# soon as the pod is "ready."
#
function blockUntilPodIsReady() {
  local label="$1"
  local secs="$2"
  local friendlyPodName="$3"

  echo -n "Waiting for \"${friendlyPodName}\" to be ready: "
  until [[ $(podStatus "${label}") =~ "true" ]]; do
    if [ "$secs" -eq 0 ]; then
      echo "\"${friendlyPodName}\" never stabilized."
      exit 1
    fi

    : $((secs--))
    echo -n .
    sleep 1
  done
}

blockUntilPodIsReady "k8s-app=kube-dns" 120 "kube-dns"  # Block until kube-dns is running &amp; ready

</code></pre></div></div>

<h1 id="cost-analysis">Cost analysis</h1>

<p>Depending on your familiarity with <code class="language-plaintext highlighter-rouge">jq</code>, <code class="language-plaintext highlighter-rouge">kubectl</code> and <code class="language-plaintext highlighter-rouge">bash</code>, writing this script could take
between half an hour up to several hours. It doesn’t look much harder to read than modern typed
languages such as golang.</p>

<p>Some of the problems with using bash include:</p>

<ul>
  <li>
    <p>Namespaces: bash code usually lives in a single file, and there’s obviously no object-oriented
encapsulation or scope. Using <code class="language-plaintext highlighter-rouge">local</code> helps a bit since bash variables are global by default.</p>
  </li>
  <li>
    <p>Unit and functional tests: writing tests seems possible in theory, but I admit I’ve never
enjoyed writing bash tests … which brings us to the final point.</p>
  </li>
  <li>
    <p>Writing bash is not fun. I’ve never experienced that feeling of easy and flow with bash that
comes from writing a beautiful C++ or golang program</p>
  </li>
</ul>

<p>Next week, the golang equivalent using <a href="https://github.com/kubernetes/client-go">client-go</a>.</p>]]></content><author><name></name></author><category term="kubernetes" /><summary type="html"><![CDATA[TL;DR Bash is rightly critized as a train wreck when it comes to writing readable, maintainable scripts. See Stop writing shell Scripts! for a sample.]]></summary></entry><entry><title type="html">Google App Engine - suitable for static sites?</title><link href="https://bcreane.github.io//google/2018/04/04/google-app-engine.html" rel="alternate" type="text/html" title="Google App Engine - suitable for static sites?" /><published>2018-04-04T03:41:11+00:00</published><updated>2018-04-04T03:41:11+00:00</updated><id>https://bcreane.github.io//google/2018/04/04/google-app-engine</id><content type="html" xml:base="https://bcreane.github.io//google/2018/04/04/google-app-engine.html"><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>Google App Engine is very easy to provision - automatic tls certs, friendly
domain name, caching, scaling and robust debugging are among the useful features.</p>

<p>However using GAE to serve a static Jekyll site (admittedly not it’s most
important use-case) has issues ranging from annoying to serious.</p>

<p>Before deploying a static site on GAE, make sure the issues outlined below
are not blockers for you.</p>

<h1 id="deploying-your-jekyll-site-on-gae">Deploying your Jekyll site on GAE</h1>

<p><a href="https://jekyllrb.com">Jekyll</a> creates a <code class="language-plaintext highlighter-rouge">_site/</code> directory with all of your site’s markdown rendered
to <code class="language-plaintext highlighter-rouge">.html</code>. Google App Engine takes a <a href="https://cloud.google.com/appengine/docs/standard/python/config/appref">app.yaml</a>
description of your site and publishes the relevant files.</p>

<h1 id="gae-issue-differentiating-files-and-directories">GAE issue: differentiating files and directories</h1>

<p>One nice feature of <a href="https://jekyllrb.com">Jekyll</a>
is <a href="https://jekyllrb.com/docs/permalinks/#builtinpermalinkstyles">Permalink style</a>
which let’s you link to your pages without annoying suffixes such as <code class="language-plaintext highlighter-rouge">.html</code>.
Unfortunately GAE doesn’t handle this well.</p>

<p>Most webservers can handle all three of the following scenarios:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>URL</th>
      <th>Proper Webserver Serves</th>
      <th>GAE Serves</th>
      <th>Makes sense?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>directory</td>
      <td>_site/fun/</td>
      <td>_site/fun/index.html</td>
      <td>_site/fun/index.html</td>
      <td>yep</td>
    </tr>
    <tr>
      <td>directory</td>
      <td>_site/fun</td>
      <td>_site/fun/index.html</td>
      <td>_site/fun.html</td>
      <td>???</td>
    </tr>
    <tr>
      <td>file</td>
      <td>_site/fun</td>
      <td>_site/fun.html</td>
      <td>_site/fun.html</td>
      <td>yep</td>
    </tr>
  </tbody>
</table>

<p>There is a workaround … you have to tell GAE about every directory and file in your site!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash

# Script to generate the site yaml for GAE to serve your site
# Get all the unique file suffixes in the _site; e.g. ".txt", ".png".
# Output in form "js|html|yml", etc.
suffixes=`find _site -type f -iname \*.* -print | sed 's/.*\.//' | sort | uniq | paste -sd "|" -`

# Create a static file handler based on all the suffixes
printf -- "- url: /(.*\\.(%s))$\n" $suffixes
printf "  static_files: _site/\\\1\n"
printf "  upload: _site/(.*\\.(%s))$\n" $suffixes

</code></pre></div></div>

<p>Now let’s handle the case where someone forgets to append a <code class="language-plaintext highlighter-rouge">/</code> to a directory URL,
for example: <code class="language-plaintext highlighter-rouge">https://_site/fun</code> when they mean to say <code class="language-plaintext highlighter-rouge">https://_site/fun/</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash

# Get all the directories in the _site; e.g. "fun"
# Output in form "/dir1|/dir2", etc.
directories=`find _site -type d -print | sed 's/_site\///g' | sort | uniq | grep -v _site | paste -sd "|" -`

# Create a handler for URLs with a directory that do NOT have a terminal /.
printf "\n"
printf "# Handle any directory URLs that are missing a terminal /\n"
printf "#\n"
printf -- "- url: /(%s)$\n" $directories
printf "  static_files: _site/\\\1/index.html\n"
printf "  upload: _site/(%s)/index.html\n" $directories
</code></pre></div></div>

<p>Okay, this is awkward and doesn’t scale, but fortunately we have a pretty small site
with fewer than 100 directories and 500 files. The generated <code class="language-plaintext highlighter-rouge">app.yaml</code> is big, but
not impossibly big.</p>

<h1 id="gae-issue-mime-type-for-scripts-or-yaml">GAE issue: mime type for scripts or yaml?</h1>

<p>Most webservers give you a robust pre-populated list of mime types. There
are only about a dozen file suffixes in our site, e.g. <code class="language-plaintext highlighter-rouge">yaml</code>, <code class="language-plaintext highlighter-rouge">txt</code>, <code class="language-plaintext highlighter-rouge">html</code>, <code class="language-plaintext highlighter-rouge">jpg</code>.
Nothing exotic.</p>

<p>Turns out GAE’s webserver doesn’t know what to do with <code class="language-plaintext highlighter-rouge">.bash</code>, <code class="language-plaintext highlighter-rouge">.sh</code>, or <code class="language-plaintext highlighter-rouge">.yaml</code>.
Again, not a big deal, but our script is getting bigger:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Serve up static files based on suffix. See:
#   https://www.iana.org/assignments/media-types/media-types.xhtml
#
# Specify mime-type for yaml files since GAE doesn't handle this correctly.
#
- url: /(.*\.(yaml|yml))$
  static_files: _site/\1
  mime_type: text/x-yaml
  upload: _site/(.*\.(yaml|yml))$

# Specify mime-type for sh files since GAE doesn't handle this correctly.
#
- url: /(.*\.(sh|bash))$
  static_files: _site/\1
  mime_type: text/x-shellscript
  upload: _site/(.*\.(sh|bash))$

# For all remaining files, let GAE infer mime-type
</code></pre></div></div>

<h1 id="gae-issue-gae-sends-compressed-files-unexpectedly">GAE issue: GAE sends compressed files, unexpectedly</h1>

<p>This one drove me crazy for a little while. Trying to download a yaml file:
<code class="language-plaintext highlighter-rouge">curl -O https://my.site.io/my_kubernetes_manifest.yaml</code> returned binary junk in the file!</p>

<p>It turns out GAE <em>sometimes</em> sends back gzipped data, even if the request header of
the client doesn’t say it can accept it. But only on <code class="language-plaintext highlighter-rouge">https</code>, not <code class="language-plaintext highlighter-rouge">http</code>. And only
sometimes.</p>

<p>There’s a one-year old bug <a href="https://issuetracker.google.com/issues/37938470">Google frontend serves gzipped content even if the client doesn’t ask for it</a> which captures this issue perfectly.</p>

<p>There are other interesting, and unresolved bugs around this area too. Try the handy <a href="https://issuetracker.google.com/issues?q=app%20engine%20curl%20https">search GAE issues</a>.</p>

<p>The workaround is to force curl to expect a gzipped response:
<code class="language-plaintext highlighter-rouge">curl --compressed -O https://my.site.io/my_kubernetes_manifest.yaml</code>.</p>

<h1 id="conclusion">Conclusion</h1>

<p>GAE is a subpar approach for serving static content for the reasons outlined above (and others).
More worryingly, the slow response to outstanding bugs implies Google’s attention is elsewhere.</p>

<p>Justin Krause did a nice job of <a href="https://hackernoon.com/going-gae-our-experience-with-google-app-engine-deaf2b7171c1">reviewing GAE for python dynamic sites</a>. GAE covered their needs nicely. As long as you’re using GAE for exactly what
it was designed for, and you don’t mind working around bugs, GAE may work for you.</p>

<h2 id="serving-a-jekyll-site-with-google-app-engine">Serving a Jekyll site with Google App Engine</h2>

<p>Here’s the bash script for generating the GAE site yaml in it’s entirety.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/bin/bash

# Script that inspects the jekyll-generated _site and emits
# a Google App Engine service specification file on stdout.

if [ ! -d _site ]; then
  echo _site/ doesn\'t exist yet, make sure you run \"jekyll build\".
  exit 1
fi

cat &lt;&lt;EOF
# A Google Application Engine "service" definition. The reference
# to python is a red-herring - we're just statically serving the
# contents of the _site subdirectory. GAE isn't setup to do this
# easily - we need to tell it how to handle each type of file, as
# well as infer "index.html" when we just refer to a site directory.
#
runtime: python27
api_version: 1
threadsafe: yes

handlers:
# Serve up static files based on suffix. See:
#   https://www.iana.org/assignments/media-types/media-types.xhtml
#
# Specify mime-type for yaml files since GAE doesn't handle this correctly.
#
- url: /(.*\.(yaml|yml))$
  static_files: _site/\1
  mime_type: text/x-yaml
  upload: _site/(.*\.(yaml|yml))$

# Specify mime-type for sh files since GAE doesn't handle this correctly.
#
- url: /(.*\.(sh|bash))$
  static_files: _site/\1
  mime_type: text/x-shellscript
  upload: _site/(.*\.(sh|bash))$

# For all remaining files, let GAE infer mime-type
#
EOF

# Get all the unique file suffixes in the _site; e.g. ".txt", ".png".
# Output in form "js|html|yml", etc.
suffixes=`find _site -type f -iname \*.* -print | sed 's/.*\.//' | sort | uniq | paste -sd "|" -`

# Create a static file handler based on all the suffixes
printf -- "- url: /(.*\\.(%s))$\n" $suffixes
printf "  static_files: _site/\\\1\n"
printf "  upload: _site/(.*\\.(%s))$\n" $suffixes

# Enumerate all the directories in the _site; e.g. "fun/". Output in form "/dir1|/dir2", etc.
directories=`find _site -type d -print | sed 's/_site\///g' | sort | uniq | grep -v _site | paste -sd "|" -`

# Create a handler for URLs with a directory that do NOT have a
# terminal /. This is a fail-safe in case someone misconstructs
# the URL without a terminal /.
printf "\n"
printf "# Handle any directory URLs that are missing a terminal /\n"
printf "#\n"
printf -- "- url: /(%s)$\n" $directories
printf "  static_files: _site/\\\1/index.html\n"
printf "  upload: _site/(%s)/index.html\n" $directories

cat &lt;&lt;EOF

# Default directory/html append rules
#
- url: /
  static_files: _site/index.html
  upload: _site/index.html

# For directories indicated by a terminal /, append "/index.html"
- url: /(.+)/
  static_files: _site/\1/index.html
  upload: _site/(.+)/index.html
  expiration: "15m"

# For md files, append ".html"
- url: /(.+[a-z0-9])
  static_files: _site/\1.html
  upload: _site/(.+[a-z]).html
  expiration: "15m"

- url: /(.+)
  static_files: _site/\1/index.html
  upload: _site/(.+)/index.html
  expiration: "15m"

- url: /(.*)
  static_files: _site/\1
  upload: _site/(.*)

libraries:
- name: webapp2
  version: "2.5.2"
EOF

</code></pre></div></div>]]></content><author><name></name></author><category term="google" /><summary type="html"><![CDATA[TL;DR Google App Engine is very easy to provision - automatic tls certs, friendly domain name, caching, scaling and robust debugging are among the useful features.]]></summary></entry></feed>