jeeb.ukI am James and below are some words on Software Engineering as well as some programming-related projects.2024-01-19T00:00:00Zhttps://jeeb.uk/James Crossjamescross265@gmail.comUsing Application Passwords with Wordpress Docker2024-01-19T00:00:00Zhttps://jeeb.uk/posts/wordpress-docker-app-passwords/<p>I want to be able to use <a href="https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#basic-authentication-with-application-passwords" target="_blank" rel="noopener noreferrer">Application Passwords</a> with my local network Wordpress installation that was using the official Docker image. There doesn't seem to be a huge amount online about how to do this so I thought I would post the solution that worked for me:</p>
<p>As I am not using https (because the service is only exposed internally within my network), I need to set an environment variable called <code>WP_ENVIRONMENT_TYPE</code> within Wordpress to "local". I can do this from my docker compose file using the <code>WORDPRESS_CONFIG_EXTRA</code> variable:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">services</span><span class="token punctuation">:</span><br /> <span class="token key atrule">wordpress</span><span class="token punctuation">:</span><br /> <span class="token key atrule">image</span><span class="token punctuation">:</span> wordpress<br /> <span class="token key atrule">environment</span><span class="token punctuation">:</span><br /> <span class="token key atrule">WORDPRESS_DB_HOST</span><span class="token punctuation">:</span> wordpress<span class="token punctuation">-</span>db<br /> <span class="token key atrule">WORDPRESS_DB_USER</span><span class="token punctuation">:</span> wordpress<br /> <span class="token key atrule">WORDPRESS_DB_PASSWORD</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>SQL_PASSWORD<span class="token punctuation">}</span><br /> <span class="token key atrule">WORDPRESS_DB_NAME</span><span class="token punctuation">:</span> wordpress<br /> <span class="token key atrule">WORDPRESS_CONFIG_EXTRA</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string"><br /> define('WP_ENVIRONMENT_TYPE', 'local');</span><br /> <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> $<span class="token punctuation">{</span>DOCKER_APP_DATA<span class="token punctuation">}</span>/wordpress/html<span class="token punctuation">:</span>/var/www/html</code></pre>
<p>Once this is done, you can navigate to your to the user admin part of wp-admin and edit a user. At the bottom of the page you can now create Application Passwords for that user.</p>
WhatsApp ChatGPT Auto-Reply2024-01-07T00:00:00Zhttps://jeeb.uk/posts/whats-app-ai-reply/<p>Wouldn't it be great to have ChatGPT auto-reply to some of your WhatsApp messages on your behalf? Well with a combination of the (unofficial) <a href="https://wwebjs.dev/" target="_blank" rel="noopener noreferrer">WhatsApp client library</a> and <a href="https://github.com/openai/openai-node" target="_blank" rel="noopener noreferrer">OpenAI client library</a> we can.</p>
<video autoplay="true" muted="true" loop="true" style="max-width: 100%">
<source src="https://jeeb.uk/assets/whats-app-ai-reply/WhatsAppAIReply.mp4" type="video/mp4" />
</video>
<p>We can do all this in under a 100 lines of code.</p>
<blockquote>
<p><strong>TLDR:</strong><br />
Full source code: <a href="https://github.com/jmc265/whatsapp-ai-reply" target="_blank" rel="noopener noreferrer">https://github.com/jmc265/whatsapp-ai-reply</a></p>
</blockquote>
<p>First, let's get some initialisation code down to get the WhatsApp client library connected to your WhatsApp account. The below code will also store the auth information to <code>/mnt/auth-data</code> so that we can restart the process and still remain connected:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> qrcode <span class="token keyword">from</span> <span class="token string">'qrcode-terminal'</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token punctuation">{</span> Client<span class="token punctuation">,</span> LocalAuth <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'whatsapp-web.js'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> whatsAppClient <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Client</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> authStrategy<span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">LocalAuth</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> dataPath<span class="token operator">:</span> <span class="token string">'/mnt/auth-data'</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span><br /> puppeteer<span class="token operator">:</span> <span class="token punctuation">{</span><br /> args<span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'--no-sandbox'</span><span class="token punctuation">]</span><span class="token punctuation">,</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />whatsAppClient<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'qr'</span><span class="token punctuation">,</span> qr <span class="token operator">=></span> <span class="token punctuation">{</span><br /> qrcode<span class="token punctuation">.</span><span class="token function">generate</span><span class="token punctuation">(</span>qr<span class="token punctuation">,</span> <span class="token punctuation">{</span> small<span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />whatsAppClient<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'ready'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'WhatsApp AI Reply is ready'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br />whatsAppClient<span class="token punctuation">.</span><span class="token function">initialize</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Next we need to listen to reaction events. We only care about events that:</p>
<ul>
<li>Are sent by ourselves</li>
<li>Are reactions to text-based messages (not images, audio etc.)</li>
</ul>
<pre class="language-typescript" tabindex="0"><code class="language-typescript">whatsAppClient<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">'message_reaction'</span><span class="token punctuation">,</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>whatsAppReaction<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>whatsAppReaction<span class="token punctuation">.</span>senderId <span class="token operator">!==</span> <span class="token string">'xxxxxx@c.us'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>whatsAppReaction<span class="token punctuation">.</span>reaction <span class="token operator">||</span> whatsAppReaction<span class="token punctuation">.</span>reaction <span class="token operator">===</span> <span class="token string">''</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">const</span> whatsAppMessage <span class="token operator">=</span> <span class="token keyword">await</span> whatsAppClient<span class="token punctuation">.</span><span class="token function">getMessageById</span><span class="token punctuation">(</span>whatsAppReaction<span class="token punctuation">.</span>msgId<span class="token punctuation">.</span>_serialized<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>whatsAppMessage<span class="token punctuation">.</span>type <span class="token operator">!==</span> MessageTypes<span class="token punctuation">.</span><span class="token constant">TEXT</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token operator">...</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Lastly, let's ask GPT4 for a response to the message and send it back:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">const</span> chatCompletion <span class="token operator">=</span> <span class="token keyword">await</span> openai<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> messages<span class="token operator">:</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span> role<span class="token operator">:</span> <span class="token string">'system'</span><span class="token punctuation">,</span> content<span class="token operator">:</span> <span class="token string">'You are an assistant who expands an emoji response to a message into a full text response'</span> <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span> role<span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span> content<span class="token operator">:</span> whatsAppMessage<span class="token punctuation">.</span>body <span class="token punctuation">}</span><span class="token punctuation">,</span><br /> <span class="token punctuation">{</span> role<span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span> content<span class="token operator">:</span> whatsAppReaction<span class="token punctuation">.</span>reaction <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><span class="token punctuation">,</span><br /> model<span class="token operator">:</span> <span class="token string">'gpt-4-1106-preview'</span><span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">if</span> <span class="token punctuation">(</span>chatCompletion<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">await</span> whatsAppMessage<span class="token punctuation">.</span><span class="token function">reply</span><span class="token punctuation">(</span>chatCompletion<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>And that's all the code we need!</p>
<p>The full source code is available at <a href="https://github.com/jmc265/whatsapp-ai-reply" target="_blank" rel="noopener noreferrer">github.com/jmc265/whatsapp-ai-reply</a>. We can wrap this up in a <a href="https://github.com/jmc265/whatsapp-ai-reply/blob/main/Dockerfile" target="_blank" rel="noopener noreferrer">Dockerfile</a> and also use <a href="https://github.com/jmc265/whatsapp-ai-reply/blob/main/compose.yml" target="_blank" rel="noopener noreferrer">Docker Compose</a> if we want.</p>
Using `for_each` in Terraform's `dynamic` blocks2023-08-21T00:00:00Zhttps://jeeb.uk/posts/terraform-dynamic-foreach/<p>The <code>for_each</code> instruction in Terraform allows you to loop a resource or module over a set to create multiple instances. For instance, if you were to do:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">variable<span class="token type variable"> "vm_instances" </span></span><span class="token punctuation">{</span><br /> <span class="token property">type</span> <span class="token punctuation">=</span> map(object(<span class="token punctuation">{</span><br /> <span class="token property">location</span> <span class="token punctuation">=</span> string<br /> <span class="token punctuation">}</span>))<br /> <span class="token property">default</span> <span class="token punctuation">=</span> <span class="token punctuation">{</span><br /> <span class="token property">instance1</span> <span class="token punctuation">=</span> <span class="token punctuation">{</span><br /> <span class="token property">location</span> <span class="token punctuation">=</span> <span class="token string">"East US"</span><br /> <span class="token punctuation">}</span>,<br /> <span class="token property">instance2</span> <span class="token punctuation">=</span> <span class="token punctuation">{</span><br /> <span class="token property">location</span> <span class="token punctuation">=</span> <span class="token string">"West US"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"azurerm_virtual_machine"</span></span> <span class="token string">"vm_instances"</span> <span class="token punctuation">{</span><br /> <span class="token property">for_each</span> <span class="token punctuation">=</span> var.vm_instances<br /><br /> <span class="token property">name</span> <span class="token punctuation">=</span> each.key<br /> <span class="token property">location</span> <span class="token punctuation">=</span> each.value.location<br /> ...<br /><span class="token punctuation">}</span></code></pre>
<p>You would create 2 VMs, one in East US, the other in West US.</p>
<p>However, it is also possible to use the <code>for_each</code> function to loop <code>dynamic</code> blocks within a resource.</p>
<p>As an example of this, I need to create 5 <code>restriction</code> blocks in the <a href="https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs/resources/schedule" target="_blank" rel="noopener noreferrer">PagerDuty Schedule resource</a> type:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"pagerduty_schedule"</span></span> <span class="token string">"in_hours_schedule"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"In Hours"</span><br /><br /> <span class="token keyword">layer</span> <span class="token punctuation">{</span> <br /> dynamic <span class="token string">"restriction"</span> <span class="token punctuation">{</span><br /> <span class="token property">for_each</span> <span class="token punctuation">=</span> toset(range(<span class="token number">1</span>, <span class="token number">6</span>))<br /><br /> <span class="token keyword">content</span> <span class="token punctuation">{</span><br /> <span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"weekly_restriction"</span><br /> <span class="token property">start_day_of_week</span> <span class="token punctuation">=</span> restriction.value<br /> ...<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>There are a couple of things to call out here:</p>
<ul>
<li><code>for_each = toset(range(1, 6))</code> - This iterates from 1 to 5 creating an array of <code>number</code> type</li>
<li><code>restriction.value</code> - When <code>for_each</code> is used in a resource, we would generally refer to the iterator with <code>each.value</code> (or <code>each.key</code>). However, within a <code>dynamic</code> block, the syntax changes and we have to use the name of the <code>dynamic</code> block. Hence in this case <code>restriction.value</code> refers to the number that the iterator is currently selecting (in this case the numbers 1 to 5).</li>
</ul>
Pair Programming is difficult2022-06-23T00:00:00Zhttps://jeeb.uk/posts/pair-programming-is-difficult/<hr />
<p><small><a href="https://www.reddit.com/r/programming/comments/vj52oi/pair_programming_is_difficult/" target="_blank" rel="noopener noreferrer">Reddit comment thread on this post</a><br />
</small></p>
<hr />
<p>Pair programming is difficult to practice effectively. Unfortunately, the inverse is also true: <strong>pair programming is easy to practice ineffectively</strong>. It is a learnt skill which is hard to pick up. I have tried to articulate some advice below for both the Driver and Navigator roles:</p>
<h3><a id="general" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/pair-programming-is-difficult/#general" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>General</h3>
<ul>
<li>Agree a <strong>destination (goal)</strong> before you start
<ul>
<li>Everyone needs to be working towards the same outcome</li>
</ul>
</li>
<li>Agree an <strong>Estimated Time of Arrival (time limit)</strong> before you start
<ul>
<li>Pair Programming is tiring. Make sure to take a break</li>
</ul>
</li>
<li>There must be <strong>exactly 2 people and 2 roles</strong>:
<ul>
<li>2 people and 1 role is wrong
<ul>
<li>2 drivers grabbing at the wheel will crash the car</li>
<li>2 navigators will never drive anywhere (possibly this is a design session)</li>
</ul>
</li>
<li>3+ people is wrong
<ul>
<li>This will result in 1 driver and 2+ navigators. The driver will become disorientated trying to listen to 2 or more sets of directions. <a href="https://en.wiktionary.org/wiki/too_many_cooks_spoil_the_broth" target="_blank" rel="noopener noreferrer">"Too many cooks spoil the broth"</a></li>
<li><strong>Update:</strong> It has since been pointed out to me that this could be something called "mob programming". That would require an entire new post by itself, but from a thousand foot view, it sounds horrible. Unless surrounded with lots of rules of interaction.</li>
</ul>
</li>
</ul>
</li>
<li>Both people must be able to <strong>access a keyboard</strong> at all times during the session
<ul>
<li>Sometimes it is easier for the navigator to show their thoughts with actions rather than just words</li>
<li>It makes it easier to switch roles (which should be done often)</li>
<li>If the session is in person, plug 2 keyboards into the same computer. If done remotely use something like <a href="https://code.visualstudio.com/learn/collaboration/live-share" target="_blank" rel="noopener noreferrer">VSCode Live Share</a></li>
</ul>
</li>
<li>Don't worry about <strong>matching novices with experts</strong>. Certainly that pairing can help with up-skilling but pairs of novices have advantages over solo programming and pairs of experts can solve complex problems.</li>
<li>Pair programming is <strong>no substitute for code reviews</strong>. Pair programming is a synchronous activity and therefore will suffer from <a href="https://en.wikipedia.org/wiki/Groupthink" target="_blank" rel="noopener noreferrer">Groupthink</a>. Code reviews are an asynchronous activity which gives the reviewer time and space to form their own opinions and thoughts.</li>
<li>Pair programming is <strong>no substitute for knowledge sharing</strong>. Where applicable, the knowledge should be shared with the whole team, not just 2 people.</li>
</ul>
<h3><a id="driver-role-" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/pair-programming-is-difficult/#driver-role-" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Driver role 🚗</h3>
<ul>
<li><strong>Don't drive too fast</strong> for the navigator. They need to understand where the car currently is located in order to direct properly</li>
<li><strong>Listen to directions</strong> from your navigator! If the navigator says it is time to write a unit test, do it. If they say it is time for a refactor, pause and consider that option with them.</li>
</ul>
<h3><a id="navigator-role-" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/pair-programming-is-difficult/#navigator-role-" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Navigator role 🗺</h3>
<ul>
<li><strong>Think about the bigger picture</strong>. This is the main role of a navigator. How are we going to get to the pre-agreed destination? Do we need to start writing some tests now? Do we need to pause and consider the implications of our change on a different part of the code base? Is it time for a break ☕️?</li>
<li><strong>Don't be a back-seat driver</strong>. Pointing out typos is generally unnecessary as the driver likely already knows the problem and the IDE/compiler will probably tell them anyway. Stylistic comments also should be avoided as it breaks the flow of both driver & navigator and this should be solved through dev tooling.</li>
</ul>
Using GCP free tier VM for uptime and health checks2022-05-26T00:00:00Zhttps://jeeb.uk/posts/gcp-free-tier-watcher/<p><img src="https://jeeb.uk/assets/pluto-uptime.png" alt="Uptime Kuma" /></p>
<p>I <a href="https://jeeb.uk/self-hosting/what-i-self-host">host a bunch of services</a> on a server in my house, which I call Jupiter (<a href="https://jeeb.uk/self-hosting/device-naming">Why "Jupiter"?</a>). I also have cron jobs on the server which run important things like <a href="https://jeeb.uk/self-hosting/backups">backups of my documents and photos/videos</a>. I need to know (immediately) if:</p>
<ul>
<li>Jupiter goes down</li>
<li>One of the services on Jupiter is not running</li>
<li>One of the cron jobs on Jupiter has not run on time or successfully</li>
</ul>
<p>Running health checks or down-detectors on Jupiter would not be wise as they might not notify me if the whole server was to go down. So I need a 3rd party, outside of my home network, to keep an eye on everything. The watcher would be very compute un-intensive, only hosting a couple of small docker images which don't generally do much, and so I went on the hunt for the cheapest, tiniest VMs/VPSs I could find:</p>
<ul>
<li><strong>Azure</strong> - B1ls (1vCPU, 512MB memory, 4GB storage) = ~£3.50pm</li>
<li><strong>GCP</strong> - e2-micro (2vCPU, 1GB memory, 10GB storage) = ~£6pm</li>
<li><strong>AWS</strong> - t4g.nano (2vCPU, 512MB memory, 10GB storage) = ~£2pm</li>
<li><strong>Hetzner</strong> - CX11 (1vCPU, 2GB memory, 20GB storage) = ~£3.50pm</li>
<li><strong>OVH</strong> - Starter (1vCPU, 2GB memory, 20GB storage) = ~£3pm</li>
</ul>
<p>These costs are all rough estimates, and don't take into account things like paying for reserved capacity up-front, additional costs for public IPs & egress traffic costs. But they give a good indicator that at the very least, it would be a cost of around £3pm.</p>
<p>But then I remembered that some of the bigger cloud providers offer "always free" tiers. And they have compute resources as part of that offering:</p>
<ul>
<li><strong><a href="https://jeeb.uk/posts/gcp-free-tier-watcher/">Azure Free Tier</a></strong> - Azure App Service (10 apps with 1GB free)</li>
<li><strong><a href="https://aws.amazon.com/free" target="_blank" rel="noopener noreferrer">AWS Free Tier</a></strong> - AWS Lambda (1 Million requests per month)</li>
<li><strong><a href="https://www.oracle.com/uk/cloud/free/" target="_blank" rel="noopener noreferrer">Oracle Cloud Free Tier</a></strong> - VM (1/8 OCPU, 1GB memory)</li>
<li><strong><a href="https://cloud.google.com/free" target="_blank" rel="noopener noreferrer">GCP Free Tier</a></strong> - VM (e2-micro, 30GB storage + free external IP)</li>
</ul>
<p>I attempted to use Azure's offering at first (as I already use Azure to host my backups, DNS, website etc.) and the terraform for this attempt can be <a href="https://github.com/jmc265/personal-cloud/blob/6e3c8450ec31cc8fdcd3eec84b0ba02d9823b724/.cloud/app_service.tf" target="_blank" rel="noopener noreferrer">found here</a>. The service did boot up, but I ran into a number of issues with web sockets and what I believe to be CPU resource constraints on the host which meant that even this simple app took ages to load. Of the other cloud providers, GCP looked the best to me, especially because of the mention in their docs of a free external IP. So I believe I can host everything I need to in GCP, absolutely free.</p>
<h2><a id="terraform" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/gcp-free-tier-watcher/#terraform" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Terraform</h2>
<p>As with my other cloud usage, everything is controlled by Terraform, making reproducing the build simple. I won't detail the steps for getting setup with GCP and Terraform cloud, as that is detailed elsewhere (e.g. <a href="https://cloud.google.com/docs/terraform" target="_blank" rel="noopener noreferrer">here</a>).</p>
<p>Once we have setup the authorisation and provider in Terraform, we need to define our compute instance. I decided to name the VM instance "pluto" due to its tiny size:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"google_compute_instance"</span></span> <span class="token string">"pluto"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"pluto"</span><br /> <span class="token property">machine_type</span> <span class="token punctuation">=</span> <span class="token string">"e2-micro"</span><br /> <span class="token property">can_ip_forward</span> <span class="token punctuation">=</span> <span class="token string">"true"</span><br /> <span class="token property">allow_stopping_for_update</span> <span class="token punctuation">=</span> <span class="token string">"true"</span><br /><br /> <span class="token keyword">boot_disk</span> <span class="token punctuation">{</span><br /> <span class="token keyword">initialize_params</span> <span class="token punctuation">{</span><br /> <span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"pd-standard"</span><br /> <span class="token property">image</span> <span class="token punctuation">=</span> data.google_compute_image.cos.self_link<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment"># This allows for an external IP address</span><br /> <span class="token keyword">network_interface</span> <span class="token punctuation">{</span><br /> <span class="token property">network</span> <span class="token punctuation">=</span> <span class="token string">"default"</span><br /> <span class="token keyword">access_config</span> <span class="token punctuation">{</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">scheduling</span> <span class="token punctuation">{</span><br /> <span class="token property">automatic_restart</span> <span class="token punctuation">=</span> <span class="token boolean">true</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment"># https://cloud.google.com/compute/docs/images/os-details</span><br /><span class="token keyword">data <span class="token type variable">"google_compute_image"</span></span> <span class="token string">"cos"</span> <span class="token punctuation">{</span><br /> <span class="token property">project</span> <span class="token punctuation">=</span> <span class="token string">"cos-cloud"</span><br /> <span class="token property">family</span> <span class="token punctuation">=</span> <span class="token string">"cos-97-lts"</span><br /><span class="token punctuation">}</span></code></pre>
<p>I've elected to use GCP's Container-Optimized OS (COS) which (amongst other things) comes with docker already installed.</p>
<p>As the boot disk only takes up 10GB, and GCP offers 30GB per month for free, I also create a 20GB attached disk to store the Docker volumes:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"google_compute_instance"</span></span> <span class="token string">"pluto"</span> <span class="token punctuation">{</span><br /> ...<br /> <span class="token keyword">lifecycle</span> <span class="token punctuation">{</span><br /> <span class="token property">ignore_changes</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span>attached_disk<span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"google_compute_disk"</span></span> <span class="token string">"default"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"disk-app-server"</span><br /> <span class="token property">type</span> <span class="token punctuation">=</span> <span class="token string">"pd-standard"</span><br /> <span class="token property">zone</span> <span class="token punctuation">=</span> <span class="token string">"<span class="token interpolation"><span class="token punctuation">$</span><span class="token punctuation">{</span><span class="token keyword">var</span><span class="token punctuation">.</span><span class="token type variable">gcp_region</span><span class="token punctuation">}</span></span>-b"</span><br /> <span class="token property">size</span> <span class="token punctuation">=</span> <span class="token number">20</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"google_compute_attached_disk"</span></span> <span class="token string">"default"</span> <span class="token punctuation">{</span><br /> <span class="token property">disk</span> <span class="token punctuation">=</span> google_compute_disk.default.id<br /> <span class="token property">instance</span> <span class="token punctuation">=</span> google_compute_instance.pluto.id<br /><span class="token punctuation">}</span></code></pre>
<p>Finally, there need to be some firewall settings to allow HTTP(S) traffic through to the instance:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"google_compute_instance"</span></span> <span class="token string">"pluto"</span> <span class="token punctuation">{</span><br /> ...<br /> <span class="token property">tags</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"ssh"</span>, <span class="token string">"http-server"</span>, <span class="token string">"https-server"</span><span class="token punctuation">]</span><br /> ...<br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"google_compute_firewall"</span></span> <span class="token string">"http-server"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"default-allow-http"</span><br /> <span class="token property">network</span> <span class="token punctuation">=</span> <span class="token string">"default"</span><br /> <span class="token keyword">allow</span> <span class="token punctuation">{</span><br /> <span class="token property">protocol</span> <span class="token punctuation">=</span> <span class="token string">"tcp"</span><br /> <span class="token property">ports</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"80"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /> <span class="token property">source_ranges</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"0.0.0.0/0"</span><span class="token punctuation">]</span><br /> <span class="token property">target_tags</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"http-server"</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"google_compute_firewall"</span></span> <span class="token string">"https-server"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"default-allow-https"</span><br /> <span class="token property">network</span> <span class="token punctuation">=</span> <span class="token string">"default"</span><br /> <span class="token keyword">allow</span> <span class="token punctuation">{</span><br /> <span class="token property">protocol</span> <span class="token punctuation">=</span> <span class="token string">"tcp"</span><br /> <span class="token property">ports</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"443"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /> <span class="token property">source_ranges</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"0.0.0.0/0"</span><span class="token punctuation">]</span><br /> <span class="token property">target_tags</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"https-server"</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span></code></pre>
<p>This Terraform gets us our very own (free!) e2-micro instance with attached disk. But we don't yet have any apps running on it.</p>
<h2><a id="docker-compose" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/gcp-free-tier-watcher/#docker-compose" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Docker (compose)</h2>
<p>I was going to go down the route of using <a href="https://cloudinit.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer">cloud-init</a> to get the e2-micro up and running with my docker containers, but it turns out someone has already wrapped up all of that into a handy Terraform module called <a href="https://registry.terraform.io/modules/christippett/container-server/cloudinit/latest" target="_blank" rel="noopener noreferrer">container-server</a>. It also handily includes Traefik with Let's Encrypt certificate generation. Everything I need!</p>
<p>First I wrote my docker-compose to include the 2 services I wanted to host: <a href="https://github.com/louislam/uptime-kuma" target="_blank" rel="noopener noreferrer">Uptime Kuma</a> and <a href="https://healthchecks.io/" target="_blank" rel="noopener noreferrer">Healthchecks</a>:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">version</span><span class="token punctuation">:</span> <span class="token string">"3"</span><br /><br /><span class="token key atrule">services</span><span class="token punctuation">:</span><br /> <span class="token key atrule">uptime-kuma</span><span class="token punctuation">:</span><br /> <span class="token key atrule">image</span><span class="token punctuation">:</span> louislam/uptime<span class="token punctuation">-</span>kuma<br /> <span class="token key atrule">container_name</span><span class="token punctuation">:</span> uptime<span class="token punctuation">-</span>kuma<br /> <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped<br /> <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> $<span class="token punctuation">{</span>DOCKER_APP_DATA<span class="token punctuation">}</span>/uptime<span class="token punctuation">-</span>kuma<span class="token punctuation">:</span>/app/data<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> traefik.enable=true<br /> <span class="token punctuation">-</span> traefik.http.routers.uptime.rule=Host(`uptime.$<span class="token punctuation">{</span>INET_DOMAIN<span class="token punctuation">}</span>`)<br /> <span class="token punctuation">-</span> traefik.http.routers.uptime.entrypoints=websecure<br /> <span class="token punctuation">-</span> traefik.http.routers.uptime.tls=true<br /> <span class="token punctuation">-</span> traefik.http.routers.uptime.tls.certresolver=letsencrypt<br /> <span class="token punctuation">-</span> traefik.http.services.uptime.loadBalancer.server.port=3001<br /><br /> <span class="token key atrule">healthchecks</span><span class="token punctuation">:</span><br /> <span class="token key atrule">image</span><span class="token punctuation">:</span> linuxserver/healthchecks<br /> <span class="token key atrule">container_name</span><span class="token punctuation">:</span> healthchecks<br /> <span class="token key atrule">environment</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> TZ=Europe/London<br /> <span class="token punctuation">-</span> SITE_ROOT=https<span class="token punctuation">:</span>//healthchecks.$<span class="token punctuation">{</span>INET_DOMAIN<span class="token punctuation">}</span><br /> <span class="token punctuation">-</span> SITE_NAME=Health Checks<br /> <span class="token punctuation">-</span> SUPERUSER_EMAIL=$<span class="token punctuation">{</span>ADMIN_EMAIL<span class="token punctuation">}</span><br /> <span class="token punctuation">-</span> SUPERUSER_PASSWORD=$<span class="token punctuation">{</span>ADMIN_PASSWORD<span class="token punctuation">}</span><br /> <span class="token punctuation">-</span> APPRISE_ENABLED=True<br /> <span class="token punctuation">-</span> PING_BODY_LIMIT=100000<br /> <span class="token punctuation">-</span> DEBUG=False<br /> <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> $<span class="token punctuation">{</span>DOCKER_APP_DATA<span class="token punctuation">}</span>/healthchecks<span class="token punctuation">:</span>/config<br /> <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> traefik.enable=true<br /> <span class="token punctuation">-</span> traefik.http.routers.healthchecks.rule=Host(`healthchecks.$<span class="token punctuation">{</span>INET_DOMAIN<span class="token punctuation">}</span>`)<br /> <span class="token punctuation">-</span> traefik.http.routers.healthchecks.entrypoints=websecure<br /> <span class="token punctuation">-</span> traefik.http.routers.healthchecks.tls=true<br /> <span class="token punctuation">-</span> traefik.http.routers.healthchecks.tls.certresolver=letsencrypt<br /><br /><span class="token key atrule">networks</span><span class="token punctuation">:</span><br /> <span class="token key atrule">default</span><span class="token punctuation">:</span><br /> <span class="token key atrule">external</span><span class="token punctuation">:</span><br /> <span class="token key atrule">name</span><span class="token punctuation">:</span> web</code></pre>
<p>And finally, using the container-server module in Terraform:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">module<span class="token type variable"> "container-server" </span></span><span class="token punctuation">{</span><br /> <span class="token property">source</span> <span class="token punctuation">=</span> <span class="token string">"christippett/container-server/cloudinit"</span><br /> <span class="token property">version</span> <span class="token punctuation">=</span> <span class="token string">"~> 1.2"</span><br /> <span class="token property">domain</span> <span class="token punctuation">=</span> <span class="token string">"pluto.<span class="token interpolation"><span class="token punctuation">$</span><span class="token punctuation">{</span><span class="token keyword">var</span><span class="token punctuation">.</span><span class="token type variable">root_domain</span><span class="token punctuation">}</span></span>"</span><br /> <span class="token property">email</span> <span class="token punctuation">=</span> var.email_address<br /> <br /> <span class="token property">files</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token property">filename</span> <span class="token punctuation">=</span> <span class="token string">"docker-compose.yaml"</span><br /> <span class="token property">content</span> <span class="token punctuation">=</span> filebase64(<span class="token string">"<span class="token interpolation"><span class="token punctuation">$</span><span class="token punctuation">{</span><span class="token keyword">path</span><span class="token punctuation">.</span><span class="token type variable">module</span><span class="token punctuation">}</span></span>/../pluto/docker-compose.yaml"</span>)<br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">]</span><br /><br /> <span class="token property">env</span> <span class="token punctuation">=</span> <span class="token punctuation">{</span><br /> <span class="token property">TRAEFIK_API_DASHBOARD</span> <span class="token punctuation">=</span> <span class="token boolean">false</span><br /> <span class="token property">DOCKER_APP_DATA</span> <span class="token punctuation">=</span> <span class="token string">"/run/app"</span><br /> <span class="token property">ADMIN_EMAIL</span> <span class="token punctuation">=</span> var.email_address<br /> <span class="token property">ADMIN_PASSWORD</span> <span class="token punctuation">=</span> var.admin_password<br /> <span class="token property">INET_DOMAIN</span> <span class="token punctuation">=</span> <span class="token string">"pluto.<span class="token interpolation"><span class="token punctuation">$</span><span class="token punctuation">{</span><span class="token keyword">var</span><span class="token punctuation">.</span><span class="token type variable">root_domain</span><span class="token punctuation">}</span></span>"</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment"># extra cloud-init config provided to setup + format persistent disk</span><br /> <span class="token property">cloudinit_part</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token punctuation">{</span><br /> <span class="token property">content_type</span> <span class="token punctuation">=</span> <span class="token string">"text/cloud-config"</span><br /> <span class="token property">content</span> <span class="token punctuation">=</span> local.cloudinit_disk<br /> <span class="token punctuation">}</span><span class="token punctuation">]</span><br /><span class="token punctuation">}</span><br /><br /><span class="token comment"># prepare persistent disk</span><br /><span class="token keyword">locals</span> <span class="token punctuation">{</span><br /> <span class="token property">cloudinit_disk</span> <span class="token punctuation">=</span> <span class="token heredoc string"><<EOT<br />#cloud-config<br />bootcmd:<br />- fsck.ext4 -tvy /dev/sdb || mkfs.ext4 /dev/sdb<br />- mkdir -p /run/app<br />- mount -o defaults -t ext4 /dev/sdb /run/app<br />EOT</span><br /><span class="token punctuation">}</span></code></pre>
<p>All of this code can be found in my GitHub repo:</p>
<ul>
<li><a href="https://github.com/jmc265/personal-cloud/blob/main/.cloud/pluto-vm.tf" target="_blank" rel="noopener noreferrer">Terraform</a></li>
<li><a href="https://github.com/jmc265/personal-cloud/blob/main/pluto/docker-compose.yaml" target="_blank" rel="noopener noreferrer">Docker Compose</a></li>
</ul>
<h2><a id="configuration" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/gcp-free-tier-watcher/#configuration" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Configuration</h2>
<h3><a id="uptime-kuma" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/gcp-free-tier-watcher/#uptime-kuma" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Uptime Kuma</h3>
<p>I now host 2 instances of Uptime Kuma:</p>
<ul>
<li>On Pluto (VM) which pings:
<ul>
<li>Jupiter's DNS entry</li>
<li>Jupiter's exposed HTTP(S) services</li>
<li>My domains (jeeb[.co].uk, <a href="http://james.cx/" target="_blank" rel="noopener noreferrer">james.cx</a>)</li>
</ul>
</li>
<li>On Jupiter (home service) which pings:
<ul>
<li>Pluto's DNS entry</li>
<li>Pluto's exposed HTTP(S) services</li>
</ul>
</li>
</ul>
<p>As they are watching each other, I will now get a notification if one of the severs/services goes down.</p>
<h3><a id="healthchecks" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/gcp-free-tier-watcher/#healthchecks" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Healthchecks</h3>
<p>My cron jobs running on Jupiter now all ping the Healthchecks service running on Pluto. You can see an example of this <a href="https://github.com/jmc265/personal-cloud/blob/main/jupiter/jeeb-uk/crontab" target="_blank" rel="noopener noreferrer">here</a>. Healthchecks acts in a dead-man-switch way and will notify me if anything doesn't report in correctly.</p>
What's in the (domain) name?2022-05-17T00:00:00Zhttps://jeeb.uk/posts/whats-in-the-domain-name/<p>I find myself periodically going on the hunt for a new domain name every few years, and having just completed that same search again, I thought I would write up some thoughts about the process.</p>
<h2><a id="do-i-trust-the-new-gtlds-or-some-cctlds" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#do-i-trust-the-new-gtlds-or-some-cctlds" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Do I trust the "new" gTLDs or some ccTLDs?</h2>
<p>[I still find myself referring to the vanity gTLDs as "new", even though they've been around for a while now.]<br />
For this search, I wanted my domain to last. I wanted to be able to use it as an email address (and therefore login to lots of sites) in perpetuity. And therefore, I needed to have complete trust that the domain would always be mine and not be taken away or lost for whatever reason.</p>
<h3><a id="cx" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#cx" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>.cx</h3>
<p>My previous domain was <a href="https://james.cx/" target="_blank" rel="noopener noreferrer">james.cx</a> and I love it (I still own it and will do for a while yet). It is so small. It is my name with no other embellishments. The <code>.cx</code> TLD actually has some resemblance to my surname. It was perfect. Except for one thing. The <code>.cx</code> TLD is the ccTLD for the Christmas Islands. I don't live in the Christmas Islands, nor work there, nor have any association what-so-ever to it. This always stopped me from using it as an email address because I didn't have complete faith that at some point in the future this domain would be taken away from me. A similar thing <a href="https://ec.europa.eu/info/sites/default/files/eu_domain_names_en.pdf" target="_blank" rel="noopener noreferrer">happened with <code>.eu</code> domains</a> when the UK left the EU. Additionally, there was a long period of time when the <code>.cx</code> official registry website was down and there was a seemingly controversial change of ownership. Not really events to breed trust.</p>
<h3><a id="dev" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#dev" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>.dev</h3>
<p>Owned by Google, it is used by lots of people in the Software Engineering community as a vanity URL. It is not too expensive (around £11py) and the name space doesn't seem too crowded yet. This was a strong contender for my new "internet home", but eventually lost out to another TLD.</p>
<h3><a id="aiio" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#aiio" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>.ai/.io</h3>
<p>Also popular choices (but more for businesses than personal domains). They are both quite a bit more expensive than the other TLDs I was looking at. And as with the <code>.cx</code> TLD, they are both ccTLDs for countries I do not reside in (although both Anguilla and British Indian Ocean Territory are both British Overseas Territory so a bit of a closer tie to me than the Christmas Islands...)</p>
<h3><a id="com" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#com" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>.com</h3>
<p>Overcrowded. Need I say more? You are highly unlikely to find anything that is not already registered that you actually want to own.</p>
<h3><a id="couk" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#couk" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>[.co].uk</h3>
<p>I finally settled on the good old [.co].uk domain(s). A ccTLD with which I do have a tie (living in the UK). Even though <code>.co.uk</code> has been taking registrations since 1985, the top level <code>.uk</code> was only register-able from 2014, so there was the possibility of a name I wanted not having been registered there yet.<br />
The downsides are that the <code>.co.uk</code> domain is fairly crowded, being the 5th most popular TLD in the world. Also, I felt that if I wanted complete trust in using the domain as an email address, I would need to register both the <code>.uk</code> and <code>.co.uk</code> version of the domain so that a typosquatting issue would not arise in the future.</p>
<h2><a id="domain-squatters" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#domain-squatters" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Domain Squatters</h2>
<p>Oh my goodness. They. Are. Everywhere. Still (not sure what I would have expected to change). I would say that in searching for a domain I wanted, I looked at over 100 domains. About 95% of those domains which were already taken were being squatted on and were for re-sale. Considering the retail price for a <code>.uk</code> domain is ~£6py the squatters really are asking for a lot. I made contact with a couple of squatters in an attempt to buy the domains from them. One of the sellers wanted £5,500 for a domain, the other wanted around £2,500! I know it is not in Nominet's interest to do anything about this, they get paid whether there is proper content on the web or not, but I really wish it wasn't so much a first-come-first served type deal, especially when the "first-come" don't actually do anything with the purchase.</p>
<h2><a id="jeeb" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/whats-in-the-domain-name/#jeeb" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Jeeb</h2>
<p>So, of all those domain I searched for, how come I ended up on <code>jeeb[.co].uk</code>? A few reasons:</p>
<ul>
<li>Nostalgia. I owned the domain <code>jeeb.co.uk</code> from around 2001 to around 2005.</li>
<li>With only 4 letters, it is really short, especially with the repeated 'e' in the middle, it makes a very small succinct domain.</li>
<li>As this was to be a personal domain, I wanted it in some way to be close to my name. The 'J' at the start gets me that.</li>
</ul>
<p>But where did the 'Jeeb' name come from in the first place when I first registered it back in 2001?</p>
<ul>
<li><code>J</code> - First letter of my name</li>
<li><code>ee</code> - I was 11 years old at the time I created the name. The double e's represented that.</li>
<li><code>b</code> - I had just finished and really enjoy the book <a href="https://en.wikipedia.org/wiki/The_Boggart" target="_blank" rel="noopener noreferrer">The Boggart</a>. The 'B' come from the book's title.</li>
</ul>
<p>Nice and simple. And so this domain will (hopefully) be my home of content for a long while yet.</p>
Crontab-as-code2022-04-25T00:00:00Zhttps://jeeb.uk/posts/crontab-as-code/<p>There are <a href="https://github.com/bdd/runitor" target="_blank" rel="noopener noreferrer">various</a> <a href="https://gitlab.science.ru.nl/bram/sch" target="_blank" rel="noopener noreferrer">tools</a> <a href="https://github.com/pforret/crontask" target="_blank" rel="noopener noreferrer">that</a> <a href="https://github.com/dimo414/task-mon" target="_blank" rel="noopener noreferrer">either</a> wrap crontab, or offer alternatives to it. All I wanted though, was a simple solution to have my scheduled tasks defined as code (checked into a git repo) so that the tasks are reproducible and idempotent, just as Terraform is for IaaS.</p>
<p>All of my scheduled tasks are linked to services (running in docker containers), and I wanted my tasks defined alongside the service which they worked upon. Therefore, I have a directory structure as below:</p>
<pre><code>cwd
| - app1
| | - docker-compose.yml
| | - crontab
| - app2
| | - docker-compose.yml
| | - crontab
</code></pre>
<p>I then have a small script at the top-level which combines all of the individual crontab files and applies the resultant set to the system crontab:</p>
<pre class="language-shell" tabindex="0"><code class="language-shell"><span class="token shebang important">#!/bin/bash</span><br /><br /><span class="token builtin class-name">export</span> <span class="token assign-left variable">CRONTAB_FILES</span><span class="token operator">=</span><span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">find</span> <span class="token builtin class-name">.</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"crontab"</span><span class="token variable">)</span></span>"</span><br /><br /><span class="token comment"># Re-create all Crontab</span><br /><span class="token builtin class-name">echo</span> <span class="token string">"# Do not edit this file, it is autogenerated"</span> <span class="token operator">></span> tmp_crontab<br /><span class="token keyword">for</span> <span class="token for-or-select variable">CRONTAB_FILE</span> <span class="token keyword">in</span> <span class="token variable">$CRONTAB_FILES</span><br /><span class="token keyword">do</span><br /> <span class="token builtin class-name">echo</span> <span class="token string">"# From <span class="token variable">$CRONTAB_FILE</span>:"</span> <span class="token operator">>></span> tmp_crontab<br /> <span class="token function">cat</span> <span class="token variable">$CRONTAB_FILE</span> <span class="token operator">>></span> tmp_crontab<br /> <span class="token builtin class-name">printf</span> <span class="token string">"<span class="token entity" title="\n">\n</span>"</span> <span class="token operator">>></span> tmp_crontab<br /><span class="token keyword">done</span><br /><br /><span class="token function">crontab</span> tmp_crontab<br /><span class="token function">rm</span> tmp_crontab<br /><span class="token function">crontab</span> -l</code></pre>
<p>Running this script means that the crontab on my server is now defined as code and checked into my git repo allowing the tasks to be reproducible.</p>
Run All Docker Compose stacks in sub-directories2022-03-22T00:00:00Zhttps://jeeb.uk/posts/docker-compose-run-all/<p>For my home server, I have a number of <code>docker-compose.yml</code> files, one for each application I run, in a sub-directory. I wanted a way to automatically do a <code>docker-compose up -d</code> for all of the applications at once, so that I don't have to <code>cd</code> into each directory in turn.</p>
<p>My directory structure is:</p>
<pre><code>cwd
| - app1
| | - docker-compose.yml
| - app2
| | - docker-compose.yml
| | - .env
| - .env
</code></pre>
<p>Unfortunately the docker-compose command doesn't allow specifying a glob pattern to run. So a small script should do the trick:</p>
<pre class="language-shell" tabindex="0"><code class="language-shell"><span class="token builtin class-name">export</span> <span class="token assign-left variable">COMPOSE_FILES</span><span class="token operator">=</span><span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">find</span> <span class="token builtin class-name">.</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">"docker-compose.yml"</span><span class="token variable">)</span></span>"</span><br /><br /><span class="token keyword">for</span> <span class="token for-or-select variable">COMPOSE_FILE</span> <span class="token keyword">in</span> <span class="token variable">$COMPOSE_FILES</span><br /><span class="token keyword">do</span><br /> <span class="token function">docker-compose</span> -f <span class="token variable">$COMPOSE_FILE</span> --env-file ./.env up -d --build<br /><span class="token keyword">done</span></code></pre>
<p>Running this script in the top level directory will now run all of my apps up at once.</p>
Protecting endpoints with Typescript Method Decorators2022-02-17T00:00:00Zhttps://jeeb.uk/posts/decorators-protect-az-func/<p>Typescript allows for an excellent language feature called <a href="https://www.typescriptlang.org/docs/handbook/decorators.html#method-decorators" target="_blank" rel="noopener noreferrer">Method Decorators</a>. They are similar to annotations in Java and can be used to wrap or edit existing methods with new functionality. Below is an example of using Decorators to add some common functionality to endpoints, including authentication, validation and error handling.</p>
<p>The first decorator we will look at is validating that the incoming requests match our expectations, and return a Bad Request (HTTP 400), if not. The actual validation of the objects will be done using <a href="https://www.npmjs.com/package/joi" target="_blank" rel="noopener noreferrer">joi</a>.</p>
<p>We will first take a look at how we use the decorator to protect our endpoint:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token decorator"><span class="token at operator">@</span><span class="token function">ValidateBody</span></span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> name<span class="token operator">:</span> Joi<span class="token punctuation">.</span><span class="token function">string</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">required</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /><span class="token punctuation">}</span><span class="token punctuation">)</span><br /><span class="token keyword">async</span> <span class="token function">httpHandler</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Hello </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>request<span class="token punctuation">.</span>body<span class="token punctuation">.</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>When we use <code>request.body.name</code> in the httpHandler, we know that the variable is defined and has a value because of the validation decorator. But what does the decorator implementation look like?</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token generic-function"><span class="token function">ValidateBody</span><span class="token generic class-name"><span class="token operator"><</span><span class="token constant">R</span><span class="token operator">></span></span></span><span class="token punctuation">(</span>bodySchema<span class="token operator">:</span> ObjectSchema<span class="token operator"><</span><span class="token constant">B</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">(</span>target<span class="token operator">:</span> object<span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> descriptor<span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">)</span><span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> originalMethod <span class="token operator">=</span> descriptor<span class="token punctuation">.</span>value<span class="token punctuation">;</span><br /> descriptor<span class="token punctuation">.</span><span class="token function-variable function">value</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> bodySchema<span class="token punctuation">.</span><span class="token function">validate</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>body<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpError</span><span class="token punctuation">(</span>HttpStatusCodes<span class="token punctuation">.</span><span class="token constant">BAD_REQUEST</span><span class="token punctuation">,</span> result<span class="token punctuation">.</span>error<span class="token punctuation">.</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">originalMethod</span><span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> request<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> descriptor<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>The decorator first stores a reference to the <code>originalMethod</code> so that we can later call it if the validations pass. The method is then overwritten with the lambda function inside the decorator. The lambda validates the body object and throws an error if the validation fails. (The <code>HttpError</code> object is caught in another decorator which is defined below.) If the validation succeeds, the <code>originalMethod</code> is called with a <code>.apply()</code> call and the return is passed back to the caller.</p>
<p>This validation can be extended to validate not just the body but also the query parameters, path parameters or http headers.</p>
<p>The next decorator we have is a HTTP basic authentication guard:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token decorator"><span class="token at operator">@</span><span class="token function">ValidateBasicAuth</span></span><span class="token punctuation">(</span>process<span class="token punctuation">.</span>env<span class="token punctuation">.</span>basicAuthUsername<span class="token punctuation">,</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span>basicAuthPassword<span class="token punctuation">)</span><br /><span class="token keyword">async</span> <span class="token function">httpHandler</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Authenticated Endpoint</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>And the definition of the decorator:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">ValidateBasicAuth</span><span class="token punctuation">(</span>username<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> password<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">(</span>target<span class="token operator">:</span> object<span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> descriptor<span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">)</span><span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> originalMethod <span class="token operator">=</span> descriptor<span class="token punctuation">.</span>value<span class="token punctuation">;</span><br /> descriptor<span class="token punctuation">.</span><span class="token function-variable function">value</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>authorization <span class="token operator">||</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>authorization<span class="token punctuation">.</span><span class="token function">indexOf</span><span class="token punctuation">(</span><span class="token string">'Basic '</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpError</span><span class="token punctuation">(</span>HttpStatusCodes<span class="token punctuation">.</span><span class="token constant">UNAUTHORIZED</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token comment">// verify auth credentials</span><br /> <span class="token keyword">const</span> base64Credentials <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>authorization<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> credentials <span class="token operator">=</span> Buffer<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>base64Credentials<span class="token punctuation">,</span> <span class="token string">'base64'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token string">'ascii'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">const</span> <span class="token punctuation">[</span>requestUsername<span class="token punctuation">,</span> requestPassword<span class="token punctuation">]</span> <span class="token operator">=</span> credentials<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">':'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>username <span class="token operator">!==</span> requestUsername <span class="token operator">||</span> password <span class="token operator">!==</span> requestPassword<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpError</span><span class="token punctuation">(</span>HttpStatusCodes<span class="token punctuation">.</span><span class="token constant">UNAUTHORIZED</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token keyword">await</span> <span class="token function">originalMethod</span><span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> request<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> descriptor<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>The decorator is very similar to the request validation decorator in that it checks a condition and throws an exception if the condition fails. In this case, the condition is a username and password check.</p>
<p>Finally, let's deal with the <code>HttpError</code> exceptions we have thrown. We want to catch these exceptions and transform them into HTTP responses that is returned to the caller:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token decorator"><span class="token at operator">@</span><span class="token function">HttpErrorHandler</span></span><br /><span class="token keyword">async</span> <span class="token function">httpHandler</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">Promise</span><span class="token operator"><</span><span class="token builtin">string</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">HttpError</span><span class="token punctuation">(</span>HttpStatusCodes<span class="token punctuation">.</span><span class="token constant">NOT_IMPLEMENTED</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>The <code>HttpErrorHandler</code> decorator is defined as:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token generic-function"><span class="token function">HttpErrorHandler</span><span class="token generic class-name"><span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span></span></span><span class="token punctuation">(</span>target<span class="token operator">:</span> object<span class="token punctuation">,</span> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> descriptor<span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">)</span><span class="token operator">:</span> TypedPropertyDescriptor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> originalMethod <span class="token operator">=</span> descriptor<span class="token punctuation">.</span>value<span class="token punctuation">;</span><br /> descriptor<span class="token punctuation">.</span><span class="token function-variable function">value</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span>request<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span><br /> <span class="token keyword">try</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> result <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">originalMethod</span><span class="token punctuation">.</span><span class="token function">apply</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> request<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> result<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>e<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>e <span class="token keyword">instanceof</span> <span class="token class-name">HttpError</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> httpError <span class="token operator">=</span> e <span class="token keyword">as</span> HttpError<span class="token punctuation">;</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> code<span class="token operator">:</span> e<span class="token punctuation">.</span>code<span class="token punctuation">,</span><br /> body<span class="token operator">:</span> <span class="token punctuation">{</span><br /> message<span class="token operator">:</span> e<span class="token punctuation">.</span>message <span class="token operator">??</span> <span class="token keyword">null</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> code<span class="token operator">:</span> HttpStatusCodes<span class="token punctuation">.</span><span class="token constant">INTERNAL_SERVER_ERROR</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><span class="token punctuation">;</span><br /> <span class="token keyword">return</span> descriptor<span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>The decorator calls the original method in a try/catch block and caches any uncaught exceptions. If the exception is of type <code>HttpError</code>, it transforms it into a Http response with the status code that was thrown. If it is any other exception, we return a generic "Internal Server Error" response.</p>
<p>These decorators can be re-used throughout the whole codebase and removes a whole load of boiler plate which is ordinarily part of the Http Handler. They also make the code very easy to scan.</p>
Low-code Telegram bot for baby sleep and feed tracking2022-01-27T00:00:00Zhttps://jeeb.uk/posts/home-tracker/<p>My wife and I wanted to start tracking and visualising our baby's sleeping and feeding schedule to help us understand the rhythms from a day-to-day basis. There are multiple apps to do that and seemingly the most popular one is <a href="https://play.google.com/store/apps/details?id=com.huckleberry_labs.app&hl=en_GB&gl=US" target="_blank" rel="noopener noreferrer">Huckleberry</a> but at $15 per month, I thought I could make something quick and simple to allow us to get the data we needed.</p>
<p>The requirements were simple:</p>
<ul>
<li>Track when the baby has fed</li>
<li>Track when (and for how long) the baby has slept</li>
<li>A very simple interface for inputting this data (the fewer the clicks/touches the better)</li>
<li>Visualise this data in a calendar form</li>
<li>Make the thing really quickly (I don't have a lot of time to spend on this)</li>
</ul>
<p>From these requirements, the solution that sprang to mind was to put calendar entries into an <code>.ics</code> file and then have a calendar pieces of software visualise this for us (we use Google Calendar, but as iCalendar is a standard we could have used any client). So that sorted out the output of the system, but as for the input of events, I needed to build it within a few hours and it needed to be super simple to use. Again, a solution sprang to mind for our particular use case: a <a href="https://nodered.org/" target="_blank" rel="noopener noreferrer">Node-RED</a> <a href="https://telegram.org/" target="_blank" rel="noopener noreferrer">Telegram</a> bot. This made sense to me for multiple reason:</p>
<ul>
<li>We already use Telegram, no new software to install</li>
<li>Telegram is a native app, so no waiting time for websites to load</li>
<li>I already use Node-RED and have already created Telegram bots in it</li>
</ul>
<p>So after registering a new bot on the Telegram platform, I started adding nodes to my Node-RED instance. As I wanted this to be as few clicks as possible, I decided to use an inline keyboard to navigate through the options. This first flow creates the top-level options:</p>
<p><img src="https://jeeb.uk/assets/hometracker/nodered.png" alt="nodered flow" /></p>
<p>The "initial keyboard" node is as follows:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">var</span> reply_markup <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span><br /> <span class="token string-property property">"inline_keyboard"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><br /> <span class="token punctuation">{</span><br /> <span class="token string-property property">"text"</span><span class="token operator">:</span> <span class="token string">"Feeding"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"callback_data"</span><span class="token operator">:</span> <span class="token string">"FEEDING"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">,</span> <br /> <span class="token punctuation">{</span><br /> <span class="token string-property property">"text"</span><span class="token operator">:</span> <span class="token string">"Sleeping"</span><span class="token punctuation">,</span><br /> <span class="token string-property property">"callback_data"</span><span class="token operator">:</span> <span class="token string">"SLEEPING"</span><br /> <span class="token punctuation">}</span><span class="token punctuation">]</span><br /> <span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><br /><span class="token keyword">var</span> options <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">chat_id</span> <span class="token operator">:</span> msg<span class="token punctuation">.</span>payload<span class="token punctuation">.</span>chatId<span class="token punctuation">,</span><br /> <span class="token literal-property property">reply_markup</span> <span class="token operator">:</span> reply_markup<span class="token punctuation">,</span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span><br /><br />msg<span class="token punctuation">.</span>payload<span class="token punctuation">.</span>type <span class="token operator">=</span> <span class="token string">'message'</span><span class="token punctuation">;</span><br />msg<span class="token punctuation">.</span>payload<span class="token punctuation">.</span>content <span class="token operator">=</span> <span class="token string">"Track activity"</span><span class="token punctuation">;</span><br />msg<span class="token punctuation">.</span>payload<span class="token punctuation">.</span>options <span class="token operator">=</span> options<span class="token punctuation">;</span><br /><br /><span class="token keyword">return</span> <span class="token punctuation">[</span> msg <span class="token punctuation">]</span><span class="token punctuation">;</span></code></pre>
<p>This results in the bot posting 2 buttons when a user types in anything to the chat:<br />
<img src="https://jeeb.uk/assets/hometracker/telegram.png" alt="telegram" /></p>
<p>In the Javascript node above, you can see that when the user clicks on the "Sleeping" button, the <code>callback_data</code> to my Node-RED app will include the key "SLEEPING". I use this to post a further inline keyboard with additional options:<br />
<img src="https://jeeb.uk/assets/hometracker/nodered2.png" alt="nodered flow" /><br />
This results in a second message and button selection:<br />
<img src="https://jeeb.uk/assets/hometracker/telegram2.png" alt="telegram" /></p>
<p>Finally, when we click on either the 30min or 1hr buttons, I want to add an iCal entry into an <code>.ics</code> file.</p>
<p>The nodes for doing this look as follows:<br />
<img src="https://jeeb.uk/assets/hometracker/nodered3.png" alt="nodered flow" /></p>
<p>The nodes labeled <code>summary = Sleep (xxx)</code> assign some values which are then read by the <code>Add Event</code> node. Importantly they set:</p>
<ul>
<li><code>msg.summary</code> = "Sleep"</li>
<li><code>msg.startTime</code> = `$fromMillis($millis() - (30 * 60 * 1000), "[Y0001][M01][D01]T[H01][m01][s01]")</li>
<li><code>msg.endTime</code> = <code>$fromMillis($millis(), "[Y0001][M01][D01]T[H01][m01][s01]")</code></li>
</ul>
<p>The last 3 nodes then read the existing <code>.ics</code> file, append a new Event and then write the file back to disk. The <code>Add Event</code> node does all this very simple:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">var</span> lines <span class="token operator">=</span> msg<span class="token punctuation">.</span>payload<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\r?\n</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">var</span> newLines <span class="token operator">=</span> <span class="token punctuation">[</span><br /> <span class="token string">"BEGIN:VEVENT"</span><span class="token punctuation">,</span><br /> <span class="token string">"DESCRIPTION:"</span><span class="token punctuation">,</span><br /> <span class="token string">"DTEND:"</span> <span class="token operator">+</span> msg<span class="token punctuation">.</span>endTime<span class="token punctuation">,</span><br /> <span class="token string">"DTSTAMP:"</span> <span class="token operator">+</span> msg<span class="token punctuation">.</span>endTime<span class="token punctuation">,</span><br /> <span class="token string">"DTSTART:"</span> <span class="token operator">+</span> msg<span class="token punctuation">.</span>startTime<span class="token punctuation">,</span><br /> <span class="token string">"SUMMARY: "</span> <span class="token operator">+</span> msg<span class="token punctuation">.</span>summary<span class="token punctuation">,</span><br /> <span class="token string">"UID: "</span> <span class="token operator">+</span> msg<span class="token punctuation">.</span>_msgid<span class="token punctuation">,</span><br /> <span class="token string">"END:VEVENT"</span><br /><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> allLines <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token operator">...</span>lines<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token operator">...</span>newLines<span class="token punctuation">,</span> <span class="token operator">...</span>lines<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">\r\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">chatId</span><span class="token operator">:</span> msg<span class="token punctuation">.</span>chatId<span class="token punctuation">,</span><br /> <span class="token literal-property property">filename</span><span class="token operator">:</span> msg<span class="token punctuation">.</span>filename<span class="token punctuation">,</span><br /> <span class="token literal-property property">payload</span><span class="token operator">:</span> allLines<br /><span class="token punctuation">}</span></code></pre>
<p>After all this is complete, the Node-RED app then sends a message back to notify the user that everything has been tracked.</p>
<p>The complete flow, including the feeding sub-flow and a sleep timer (which creates an ical event based on the length of time between pressing "start" and "end") is:<br />
<img src="https://jeeb.uk/assets/hometracker/nodered4.png" alt="nodered flow" /></p>
<p>This was tested and works fine for adding events to the ical file. The final bit of the puzzle was getting Google Calendar to display the events. As the file was on my local server at home, I had to expose the file to the internet, and I chose nginx in a docker container to do this. Probably a bit of an overkill but I needed something quick.</p>
<p>Here are the relevant docker-compose entries:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">traefik</span><span class="token punctuation">:</span><br /> <span class="token key atrule">container_name</span><span class="token punctuation">:</span> traefik<br /> <span class="token key atrule">image</span><span class="token punctuation">:</span> traefik<span class="token punctuation">:</span>v2.5<br /> <span class="token key atrule">network_mode</span><span class="token punctuation">:</span> host<br /> <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> ./traefik.yml<span class="token punctuation">:</span>/etc/traefik/traefik.yml<br /> <span class="token punctuation">-</span> ./letsencrypt<span class="token punctuation">:</span>/letsencrypt<br /> <span class="token punctuation">-</span> /var/run/docker.sock<span class="token punctuation">:</span>/var/run/docker.sock<br /><br /><span class="token key atrule">cal-nginx</span><span class="token punctuation">:</span><br /> <span class="token key atrule">container_name</span><span class="token punctuation">:</span> cal<span class="token punctuation">-</span>nginx<br /> <span class="token key atrule">image</span><span class="token punctuation">:</span> nginx<br /> <span class="token key atrule">volumes</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> /home/user/cal.ics<span class="token punctuation">:</span>/usr/share/nginx/html/cal.ics<span class="token punctuation">:</span>z<br /> <span class="token punctuation">-</span> ./mime.types<span class="token punctuation">:</span>/etc/nginx/mime.types<br /> <span class="token key atrule">ports</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> 4400<span class="token punctuation">:</span><span class="token number">80</span><br /> <span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped<br /> <span class="token key atrule">labels</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> traefik.enable=true<br /> <span class="token punctuation">-</span> traefik.http.routers.home<span class="token punctuation">-</span>nginx.rule=Host(`cal.domain.com`)<br /> <span class="token punctuation">-</span> traefik.http.routers.home<span class="token punctuation">-</span>nginx.tls=true<br /> <span class="token punctuation">-</span> traefik.http.routers.home<span class="token punctuation">-</span>nginx.tls.certresolver=letsencrypt</code></pre>
<p>With this setup, my ics file was now exposed to <code>https://cal.domain.com/cal.ics</code>. Google Calendar allows importing from a URL and they claim that they sync this every 12 hours or so. In practice it appears to be a lot longer refresh period than that, and besides we want to see the results much quicker than that. Luckily someone has created a Google AppScript which takes an ics file and replicates the entries to your calendar as often as you like. This is <a href="https://github.com/derekantrican/GAS-ICS-Sync" target="_blank" rel="noopener noreferrer">GAS-ICS-Sync</a>. After installing and configuring the script, I can see my entries in Google Calendar:</p>
<p><img src="https://jeeb.uk/assets/hometracker/calendar.jpg" alt="calendar" /></p>
Using Azure as a Dynamic DNS provider for your home server2022-01-24T00:00:00Zhttps://jeeb.uk/posts/azure-ddns/<p>When hosting services from your home, you will want to use a Dynamic DNS (DDNS) entry in order to map your ever-changing IP address to a hostname you can use to access those services. For instance, <code>home-server.mydomain.com</code> will point to your IPv4 (or IPv6) address. There are providers <a href="https://www.noip.com/" target="_blank" rel="noopener noreferrer">noip</a> and <a href="https://www.duckdns.org/" target="_blank" rel="noopener noreferrer">Duck DNS</a>, but below is a method to use an Azure DNS zone and a script to update the IP on a regular basis. We will do this using Terraform and Docker containers.</p>
<h2><a id="terraform" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-ddns/#terraform" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Terraform</h2>
<p>I have <a href="https://jeeb.uk/2021-06-22-azure-cdn" target="_blank" rel="noopener noreferrer">previously explained</a> how to get setup and running with Terraform, Github Workflows and Azure. We will be building on top of that post and ideas.</p>
<p>The first thing you will need is an Azure zone and A Record entry:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"azurerm_dns_zone"</span></span> <span class="token string">"mydomain"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"mydomain.com"</span><br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.resource-group.name<br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"azurerm_dns_a_record"</span></span> <span class="token string">"home-server"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"home-server"</span><br /> <span class="token property">zone_name</span> <span class="token punctuation">=</span> azurerm_dns_zone.mydomain.name<br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.resource-group.name<br /> <span class="token property">ttl</span> <span class="token punctuation">=</span> <span class="token number">300</span><br /> <span class="token property">records</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"1.2.3.4"</span><span class="token punctuation">]</span> <span class="token comment"># Updated by script</span><br /><span class="token punctuation">}</span></code></pre>
<p>The IPv4 address will be updated in a script below. If you want to use IPv6, you will need an <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/dns_aaaa_record" target="_blank" rel="noopener noreferrer"><code>aaaa</code></a> record as well.</p>
<h2><a id="script" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-ddns/#script" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Script</h2>
<p>We now need a script which will run on a host within our home network to get our IP address and update the <code>a</code> record above. We will be using the <a href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli" target="_blank" rel="noopener noreferrer">azure cli</a> to update the record.</p>
<pre class="language-shell" tabindex="0"><code class="language-shell"><span class="token shebang important">#!/bin/bash</span><br /><span class="token builtin class-name">set</span> -ex<br /><br />az login --service-principal -u <span class="token variable">${AZURE_CLIENT_ID}</span> -p <span class="token variable">${AZURE_CLIENT_SECRET}</span> --tenant <span class="token variable">${AZURE_TENANT_ID}</span><br /><br /><span class="token assign-left variable">newIp</span><span class="token operator">=</span><span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">dig</span> +short myip.opendns.com @resolver1.opendns.com<span class="token variable">)</span></span>"</span><br /><span class="token assign-left variable">oldIp</span><span class="token operator">=</span><span class="token string">"<span class="token variable"><span class="token variable">$(</span>az network dns record-set a show --resource-group $<span class="token punctuation">{</span>AZURE_RESOURCE_GROUP<span class="token punctuation">}</span> --zone-name $<span class="token punctuation">{</span>AZURE_ZONE_NAME<span class="token punctuation">}</span> --name $<span class="token punctuation">{</span>AZURE_RECORD_NAME<span class="token punctuation">}</span> -o tsv --query <span class="token string">"aRecords[0].ipv4Address"</span><span class="token variable">)</span></span>"</span><br /><br /><span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$newIp</span>"</span> <span class="token operator">==</span> <span class="token string">"<span class="token variable">$oldIp</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span><br /> <span class="token builtin class-name">echo</span> <span class="token string">"IP has not been updated"</span><br /><span class="token keyword">else</span><br /> <span class="token builtin class-name">echo</span> <span class="token string">"Updating IP to <span class="token variable">${newIp}</span>"</span><br /> az network dns record-set a remove-record --resource-group <span class="token variable">${AZURE_RESOURCE_GROUP}</span> --zone-name <span class="token variable">${AZURE_ZONE_NAME}</span> --record-set-name <span class="token variable">${AZURE_RECORD_NAME}</span> --ipv4-address <span class="token variable">${oldIp}</span> --keep-empty-record-set<br /> az network dns record-set a add-record --resource-group <span class="token variable">${AZURE_RESOURCE_GROUP}</span> --zone-name <span class="token variable">${AZURE_ZONE_NAME}</span> --record-set-name <span class="token variable">${AZURE_RECORD_NAME}</span> --ipv4-address <span class="token variable">${newIp}</span><br /><span class="token keyword">fi</span></code></pre>
<p>There are some environment variables above which we will be injecting to the docker container below.</p>
<h2><a id="docker" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-ddns/#docker" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Docker</h2>
<p>First we will want a Dockerfile which wraps up the shell script:</p>
<pre class="language-dockerfile" tabindex="0"><code class="language-dockerfile"><span class="token instruction"><span class="token keyword">FROM</span> mcr.microsoft.com/azure-cli</span><br /><br /><span class="token comment"># To get the `dig` unix command</span><br /><span class="token instruction"><span class="token keyword">RUN</span> apk add --no-cache bind-tools</span><br /><br /><span class="token instruction"><span class="token keyword">ADD</span> azddns.sh /</span><br /><br /><span class="token instruction"><span class="token keyword">ENTRYPOINT</span> /azddns.sh</span></code></pre>
<p>We can then build and run this dockerfile:</p>
<pre class="language-shell" tabindex="0"><code class="language-shell"><span class="token function">docker</span> build -t azddns <span class="token builtin class-name">.</span><br /><span class="token function">docker</span> run --rm <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_CLIENT_ID</span><span class="token operator">=</span><span class="token operator"><</span>insert client id<span class="token operator">></span> <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_CLIENT_SECRET</span><span class="token operator">=</span><span class="token operator"><</span>insert client secret<span class="token operator">></span> <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_TENANT_ID</span><span class="token operator">=</span><span class="token operator"><</span>insert tenant id<span class="token operator">></span> <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_RESOURCE_GROUP</span><span class="token operator">=</span>resource-group <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_ZONE_NAME</span><span class="token operator">=</span>mydomain.com <span class="token punctuation">\</span><br /> -e <span class="token assign-left variable">AZURE_RECORD_NAME</span><span class="token operator">=</span>home-server <span class="token punctuation">\</span><br /> azddns</code></pre>
<p>Running this docker run command should be done on a regular basis. This could be done with a cron job, or by using <a href="https://github.com/mcuadros/ofelia" target="_blank" rel="noopener noreferrer">ofelia</a>.</p>
<p>Here is the docker-compose entry:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"> <span class="token key atrule">azddns</span><span class="token punctuation">:</span><br /> <span class="token key atrule">build</span><span class="token punctuation">:</span> azddns<br /> <span class="token key atrule">container_name</span><span class="token punctuation">:</span> azddns<br /> <span class="token key atrule">environment</span><span class="token punctuation">:</span><br /> <span class="token key atrule">AZURE_CLIENT_ID</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>AZURE_CLIENT_ID<span class="token punctuation">}</span><br /> <span class="token key atrule">AZURE_CLIENT_SECRET</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>AZURE_CLIENT_SECRET<span class="token punctuation">}</span><br /> <span class="token key atrule">AZURE_TENANT_ID</span><span class="token punctuation">:</span> $<span class="token punctuation">{</span>AZURE_TENANT_ID<span class="token punctuation">}</span><br /> <span class="token key atrule">AZURE_RESOURCE_GROUP</span><span class="token punctuation">:</span> <span class="token string">"resource-group"</span><br /> <span class="token key atrule">AZURE_ZONE_NAME</span><span class="token punctuation">:</span> <span class="token string">"mydomain.com"</span><br /> <span class="token key atrule">AZURE_RECORD_NAME</span><span class="token punctuation">:</span> <span class="token string">"home-server"</span></code></pre>
<p>And the entry in the ofelia config:</p>
<pre class="language-ini" tabindex="0"><code class="language-ini"><span class="token header"><span class="token punctuation">[</span><span class="token section-name selector">job-exec "az ddns"</span><span class="token punctuation">]</span></span><br /><span class="token key attr-name">schedule</span> <span class="token punctuation">=</span> <span class="token value attr-value">@hourly</span><br /><span class="token key attr-name">container</span> <span class="token punctuation">=</span> <span class="token value attr-value">azddns</span><br /><span class="token key attr-name">command</span> <span class="token punctuation">=</span> <span class="token value attr-value">/azddns.sh</span></code></pre>
<h2><a id="conclusion" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-ddns/#conclusion" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Conclusion</h2>
<p>And that's it! You could obviously alter this to use other cloud providers as well, but you now control one more aspect of your tech stack.</p>
First Drone FPV flight2021-08-28T00:00:00Zhttps://jeeb.uk/posts/first-fpv-flight/<p>This is my first attempt at flying my <a href="https://betafpv.com/collections/beta85x-series-drones/products/beta85x-v2-whoop-quadcopter" target="_blank" rel="noopener noreferrer">Beta85X V2</a> with an action cam strapped on top. It involves no small amount of crashing as well as almost getting stuck in a very tall tree, but it survived and made it home to fly another day.</p>
<p><img src="https://jeeb.uk/assets/beta85x.jpg" alt="Beta85X V2" /></p>
<p><a href="https://www.youtube.com/watch?v=E6Myad6036o" target="_blank" rel="noopener noreferrer">https://www.youtube.com/watch?v=E6Myad6036o</a></p>
Using Azure, Terraform and GitHub Actions to host an (almost) free static site2021-06-22T00:00:00Zhttps://jeeb.uk/posts/azure-cdn/<p>Let's start this post by saying that everything below is unnecessary. The outcome from this is exactly what <a href="https://pages.github.com/" target="_blank" rel="noopener noreferrer">GitHub Pages</a> gives you for (completely) free. Hosting in Azure comes with a very small cost (pricing explained below), but the point of this is to learn about Azure, Terraform and GitHub Actions in the process of hosting a small, static website whilst keeping the costs very low.</p>
<h2><a id="services" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#services" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Services</h2>
<p>So what services will we be using, and how much will they cost us?</p>
<ul>
<li>GitHub
<ul>
<li>GitHub to store code (free)</li>
<li>GitHub Flows to build and release the website (free)</li>
</ul>
</li>
<li>Terraform Cloud to create the infrastructure (free)</li>
<li>Azure
<ul>
<li>Azure Storage to store the built files (<a href="https://azure.microsoft.com/en-gb/pricing/details/storage/blobs/" target="_blank" rel="noopener noreferrer">~£0.01 per GB</a>)</li>
<li>Azure CDN to serve the content (<a href="https://azure.microsoft.com/en-gb/pricing/details/cdn/#pricing" target="_blank" rel="noopener noreferrer">~£0.06 per GB</a>)</li>
<li>Azure CDN to manage our certificate (free)</li>
<li>Azure DNS for the website DNS (<a href="https://azure.microsoft.com/en-gb/pricing/details/dns/" target="_blank" rel="noopener noreferrer">~£0.40 per month</a>)</li>
</ul>
</li>
</ul>
<p>You will first need to sign up for accounts at <a href="https://azure.microsoft.com/en-gb/free/" target="_blank" rel="noopener noreferrer">Azure</a>, <a href="https://github.com/join" target="_blank" rel="noopener noreferrer">GitHub</a> and <a href="https://app.terraform.io/signup/account" target="_blank" rel="noopener noreferrer">Terraform Cloud</a>.</p>
<h2><a id="content" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#content" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Content</h2>
<p>We will first need a <a href="https://github.com/new" target="_blank" rel="noopener noreferrer">new repository on GitHub</a> in order to push our code to. Use the instructions provided on GitHub to create or link the repository to a local folder on your computer. Within the new repository, create a <code>src</code> folder and then an <code>index.html</code> file within the folder. We will put in some placeholder content for now:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Hello, World<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span><br /> <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span><br /><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<p>Commit and Push that to the remote repository.</p>
<h2><a id="infrastructure" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#infrastructure" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Infrastructure</h2>
<p>Next, we will are going to create some Terraform files which will describe the resources we want to be created on Azure to host our site. Start by creating a new top level folder in your repository called <code>.cloud</code>.</p>
<h3><a id="setup" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#setup" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Setup</h3>
<p>Let's create a <code>main.tf</code> file in that folder with the below contents:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">terraform</span> <span class="token punctuation">{</span><br /> <span class="token keyword">backend<span class="token type variable"> "remote" </span></span><span class="token punctuation">{</span><br /> <span class="token property">hostname</span> <span class="token punctuation">=</span> <span class="token string">"app.terraform.io"</span><br /> <span class="token property">organization</span> <span class="token punctuation">=</span> <span class="token string">"MY-ORG"</span><br /><br /> <span class="token keyword">workspaces</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"static-site"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">required_providers</span> <span class="token punctuation">{</span><br /> <span class="token property">azurerm</span> <span class="token punctuation">=</span> <span class="token punctuation">{</span><br /> <span class="token property">source</span> <span class="token punctuation">=</span> <span class="token string">"hashicorp/azurerm"</span><br /> <span class="token property">version</span> <span class="token punctuation">=</span> <span class="token string">">= 2.26"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token property">required_version</span> <span class="token punctuation">=</span> <span class="token string">">= 0.14.9"</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">provider<span class="token type variable"> "azurerm" </span></span><span class="token punctuation">{</span><br /> <span class="token keyword">features</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><br /> <span class="token property">subscription_id</span> <span class="token punctuation">=</span> var.azure_subscription_id<br /> <span class="token property">client_id</span> <span class="token punctuation">=</span> var.azure_client_id<br /> <span class="token property">client_secret</span> <span class="token punctuation">=</span> var.azure_client_secret<br /> <span class="token property">tenant_id</span> <span class="token punctuation">=</span> var.azure_tenant_id<br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"azurerm_resource_group"</span></span> <span class="token string">"static-site"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"static-site"</span><br /> <span class="token property">location</span> <span class="token punctuation">=</span> <span class="token string">"uksouth"</span><br /><span class="token punctuation">}</span></code></pre>
<p>Make sure you change the <code>MY-ORG</code> to match the organisation you entered when signing up to Terraform Cloud.</p>
<p>The above sets up some important information for us, including the Terraform backend and the connection to Azure (although the credentials for that will be dealt with later). It will also create our first resource in Azure: the Resource Group within which all our other resources (CDN, DNS, Storage) will be contained.</p>
<h3><a id="storage" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#storage" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Storage</h3>
<p>The next resource we will want to create is the Storage Account which will hold our static files to be read from the CDN. Create the file <code>.cloud/storage.tf</code> with the contents:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"azurerm_storage_account"</span></span> <span class="token string">"static-site"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"staticsitestorage"</span><br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /> <span class="token property">location</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.location<br /> <span class="token property">account_tier</span> <span class="token punctuation">=</span> <span class="token string">"Standard"</span><br /> <span class="token property">account_replication_type</span> <span class="token punctuation">=</span> <span class="token string">"LRS"</span><br /> <span class="token property">enable_https_traffic_only</span> <span class="token punctuation">=</span> <span class="token boolean">true</span><br /> <span class="token keyword">static_website</span> <span class="token punctuation">{</span><br /> <span class="token property">index_document</span> <span class="token punctuation">=</span> <span class="token string">"index.html"</span><br /> <span class="token property">error_404_document</span> <span class="token punctuation">=</span> <span class="token string">"index.html"</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">blob_properties</span> <span class="token punctuation">{</span><br /> <span class="token keyword">cors_rule</span> <span class="token punctuation">{</span><br /> <span class="token property">allowed_headers</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"*"</span><span class="token punctuation">]</span><br /> <span class="token property">allowed_methods</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"GET"</span>, <span class="token string">"HEAD"</span><span class="token punctuation">]</span><br /> <span class="token property">allowed_origins</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"*"</span><span class="token punctuation">]</span><br /> <span class="token property">exposed_headers</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"*"</span><span class="token punctuation">]</span><br /> <span class="token property">max_age_in_seconds</span> <span class="token punctuation">=</span> <span class="token number">3600</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<h3><a id="cdn" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#cdn" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>CDN</h3>
<p>Now lets add the resources to create a CDN which will be backed by the Storage Account above. This will be in a new file <code>.cloud/cdn.tf</code>.</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"azurerm_cdn_profile"</span></span> <span class="token string">"static-site"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"static-site-cdn"</span><br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /> <span class="token property">location</span> <span class="token punctuation">=</span> <span class="token string">"westeurope"</span><br /> <span class="token property">sku</span> <span class="token punctuation">=</span> <span class="token string">"Standard_Microsoft"</span><br /><span class="token punctuation">}</span></code></pre>
<p>This adds the CDN "profile" into Azure which is just a container. We will now need to add the CDN "endpoint" which connects an external domain with an origin (our Storage Account):</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"azurerm_cdn_endpoint"</span></span> <span class="token string">"static-site"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"static-site-cdnep"</span><br /> <span class="token property">profile_name</span> <span class="token punctuation">=</span> azurerm_cdn_profile.static-site.name<br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /> <span class="token property">location</span> <span class="token punctuation">=</span> <span class="token string">"westeurope"</span><br /><br /> <span class="token property">origin_host_header</span> <span class="token punctuation">=</span> azurerm_storage_account.static-site.primary_web_host<br /><br /> <span class="token property">is_http_allowed</span> <span class="token punctuation">=</span> <span class="token boolean">true</span><br /> <span class="token property">is_compression_enabled</span> <span class="token punctuation">=</span> <span class="token boolean">true</span><br /><br /> <span class="token property">content_types_to_compress</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><br /> <span class="token string">"text/plain"</span>,<br /> <span class="token string">"text/html"</span>,<br /> <span class="token string">"text/css"</span>,<br /> <span class="token string">"text/javascript"</span>,<br /> <span class="token string">"application/x-javascript"</span>,<br /> <span class="token string">"application/javascript"</span>,<br /> <span class="token string">"application/json"</span>,<br /> <span class="token string">"application/xml"</span><br /> <span class="token punctuation">]</span><br /><br /> <span class="token keyword">delivery_rule</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"httpRedirect"</span><br /> <span class="token property">order</span> <span class="token punctuation">=</span> <span class="token number">1</span><br /> <span class="token keyword">request_scheme_condition</span> <span class="token punctuation">{</span><br /> <span class="token property">operator</span> <span class="token punctuation">=</span> <span class="token string">"Equal"</span><br /> <span class="token property">match_values</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"HTTP"</span><span class="token punctuation">]</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">url_redirect_action</span> <span class="token punctuation">{</span><br /> <span class="token property">redirect_type</span> <span class="token punctuation">=</span> <span class="token string">"PermanentRedirect"</span><br /> <span class="token property">protocol</span> <span class="token punctuation">=</span> <span class="token string">"Https"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">delivery_rule</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"wwwRedirect"</span><br /> <span class="token property">order</span> <span class="token punctuation">=</span> <span class="token number">2</span><br /> <span class="token keyword">request_uri_condition</span> <span class="token punctuation">{</span><br /> <span class="token property">operator</span> <span class="token punctuation">=</span> <span class="token string">"BeginsWith"</span><br /> <span class="token property">match_values</span> <span class="token punctuation">=</span> <span class="token punctuation">[</span><span class="token string">"https://www."</span><span class="token punctuation">]</span><br /> <span class="token property">transforms</span> <span class="token punctuation">=</span> <span class="token string">"Lowercase"</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">url_redirect_action</span> <span class="token punctuation">{</span><br /> <span class="token property">redirect_type</span> <span class="token punctuation">=</span> <span class="token string">"PermanentRedirect"</span><br /> <span class="token property">protocol</span> <span class="token punctuation">=</span> <span class="token string">"Https"</span><br /> <span class="token property">hostname</span> <span class="token punctuation">=</span> <span class="token string">"https://james.cx"</span><br /> <span class="token punctuation">}</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">origin</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> azurerm_storage_account.blog.name<br /> <span class="token property">host_name</span> <span class="token punctuation">=</span> azurerm_storage_account.blog.primary_web_host<br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /></code></pre>
<p>Some notes about the above block:</p>
<ul>
<li>We specifically allow HTTP traffic to our CDN, but then the first <code>delivery_rule</code> redirects all incoming HTTP requests to HTTPS.</li>
<li>We also set up a second redirection delivery rule to redirect all traffic starting with <code>www.</code> to our root (or apex) domain. More on this later.</li>
</ul>
<h3><a id="dns" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#dns" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>DNS</h3>
<p>The last part of the Terraform is to set up the DNS in <a href="http://dns.tf/" target="_blank" rel="noopener noreferrer">dns.tf</a>. We will set up 2 CNAME records for the <code>www</code> subdomain and the "apex" or "root" domains to point towards our CDN Endpoint:</p>
<pre class="language-hcl" tabindex="0"><code class="language-hcl"><span class="token keyword">resource <span class="token type variable">"azurerm_dns_zone"</span></span> <span class="token string">"jamescx"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"james.cx"</span><br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"azurerm_dns_cname_record"</span></span> <span class="token string">"www"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"www"</span><br /> <span class="token property">zone_name</span> <span class="token punctuation">=</span> azurerm_dns_zone.jamescx.name<br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /> <span class="token property">ttl</span> <span class="token punctuation">=</span> <span class="token number">300</span><br /> <span class="token property">target_resource_id</span> <span class="token punctuation">=</span> azurerm_cdn_endpoint.static-site.id<br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">resource <span class="token type variable">"azurerm_dns_a_record"</span></span> <span class="token string">"apex"</span> <span class="token punctuation">{</span><br /> <span class="token property">name</span> <span class="token punctuation">=</span> <span class="token string">"@"</span><br /> <span class="token property">zone_name</span> <span class="token punctuation">=</span> azurerm_dns_zone.jamescx.name<br /> <span class="token property">resource_group_name</span> <span class="token punctuation">=</span> azurerm_resource_group.static-site.name<br /> <span class="token property">ttl</span> <span class="token punctuation">=</span> <span class="token number">300</span><br /> <span class="token property">target_resource_id</span> <span class="token punctuation">=</span> azurerm_cdn_endpoint.static-site.id<br /><span class="token punctuation">}</span></code></pre>
<h2><a id="creating-the-resources-terraform-cloud" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#creating-the-resources-terraform-cloud" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Creating the Resources (Terraform Cloud)</h2>
<p>Now that we have our Terraform files, we need to get Terraform Cloud to create the resources into our Azure Account. The setup for this is best seen on the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs" target="_blank" rel="noopener noreferrer">Terraform provider</a> or the <a href="https://docs.microsoft.com/en-us/azure/developer/terraform/get-started-cloud-shell" target="_blank" rel="noopener noreferrer">Microsoft Docs</a> sites.</p>
<p>There is currently a manual step that will need to do with the CDN, which is to link up the domain and certificate. This is a missing feature (at time of writing) of the Terraform provider. Once the resources have been created in Azure, do these steps:</p>
<ul>
<li>Open up the CDN Endpoint in the <a href="https://portal.azure.com/" target="_blank" rel="noopener noreferrer">Azure Portal</a></li>
<li>Click "Add a Custom Domain"</li>
<li>Enter the hostname for your <code>www</code> domain (e.g. <code>www.james.cx</code>) and click Create</li>
<li>Click through to open up the custom domain settings</li>
<li>Turn On the custom domain HTTPS</li>
<li>Make sure CDN Managed and TLS 1.2 are selected</li>
<li>Click Save</li>
</ul>
<p>You will have to wait a while whilst the certificate is provisioned for you.</p>
<p>For the apex or root domain, there is an additional (and rather annoying) step. Azure will not (at time of writing) cerate you a free apex certificate so you will have to source one yourself. You can either do this for free (<a href="https://letsencrypt.org/" target="_blank" rel="noopener noreferrer">Let's Encrypt</a>) or purchase your own (I recommend <a href="https://www.namecheap.com/security/ssl-certificates/" target="_blank" rel="noopener noreferrer">Namecheap</a>). The certificate can then be uploaded to a KeyVault within Azure and used from the CDN.</p>
<h2><a id="github-flows" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#github-flows" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Github Flows</h2>
<p>Lastly, we will get Github Flows to build and deploy the static site to the created Azure Resources.</p>
<p>Create the file <code>.github/workflows/build-blog.yml</code> with the blow contents:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">name</span><span class="token punctuation">:</span> Build & Release Blog<br /><span class="token key atrule">on</span><span class="token punctuation">:</span><br /> <span class="token key atrule">push</span><span class="token punctuation">:</span><br /> <span class="token key atrule">branches</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> main<br /> <br /><span class="token key atrule">jobs</span><span class="token punctuation">:</span><br /> <span class="token key atrule">build_blog</span><span class="token punctuation">:</span><br /> <span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span>latest<br /> <span class="token key atrule">steps</span><span class="token punctuation">:</span><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> CHECKOUT<br /> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v2<br /> <br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> AZURE LOGIN <br /> <span class="token key atrule">uses</span><span class="token punctuation">:</span> azure/login@v1<br /> <span class="token key atrule">with</span><span class="token punctuation">:</span><br /> <span class="token key atrule">creds</span><span class="token punctuation">:</span> $<br /> <br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Upload to blob storage<br /> <span class="token key atrule">uses</span><span class="token punctuation">:</span> azure/CLI@v1<br /> <span class="token key atrule">with</span><span class="token punctuation">:</span><br /> <span class="token key atrule">azcliversion</span><span class="token punctuation">:</span> 2.0.72<br /> <span class="token key atrule">inlineScript</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string"><br /> az storage blob upload-batch --account-name staticsitestorage -d '$web' -s ./src</span><br /> <br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Purge CDN endpoint<br /> <span class="token key atrule">uses</span><span class="token punctuation">:</span> azure/CLI@v1<br /> <span class="token key atrule">with</span><span class="token punctuation">:</span><br /> <span class="token key atrule">azcliversion</span><span class="token punctuation">:</span> 2.0.72<br /> <span class="token key atrule">inlineScript</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string"><br /> az cdn endpoint purge --content-paths "/*" --profile-name "static-site-cdn" --name "static-site-cdnep" --resource-group "static-site"</span><br /><br /> <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> logout<br /> <span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string"><br /> az logout</span></code></pre>
<p>The steps do this: login to Azure, upload the static files and then purge the CDN cache so that the new files are visible ASAP.</p>
<p>You will also need to setup the deployment credentials within Github. Details for this can be found on the <a href="https://github.com/marketplace/actions/azure-login" target="_blank" rel="noopener noreferrer">Github Marketplace</a> site.</p>
<h2><a id="conclusion" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/azure-cdn/#conclusion" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Conclusion</h2>
<p>And that should be it! You have now setup a static site and workflow such that when you push a change to your main branch, the files will appear on your domain.</p>
3D printed stand for Pirate Audio2020-12-06T00:00:00Zhttps://jeeb.uk/posts/rpi-zero-pirate-audio/<p>As a gift, I received a <a href="https://shop.pimoroni.com/products/pirate-audio-line-out" target="_blank" rel="noopener noreferrer">Raspberry Pi Zero Pirate Audio</a>Mini HAT. I got it all set up with <a href="https://mopidy.com/" target="_blank" rel="noopener noreferrer">Mopidy</a> but I wanted to have it displayed nicely on my desk so that I could easily glance over and see the current song/artists, as well as easily access the next/previous buttons.</p>
<p>Luckily, someone on Thingiverse had already created a flat case for the <a href="https://www.thingiverse.com/thing:4087948" target="_blank" rel="noopener noreferrer">Rpi Zero with the Pirate Audio HAT attached</a>. I have added to the design to create a 45 degree built in stand. The only complex part of this is that the Rpi is now used up-side-down because the power cable needed to come out of the top for clearance. There are settings on the built-in display to allow for a 180 degree rotation.</p>
<p><img src="https://jeeb.uk/assets/rpi-audio.png" alt="rpi audio design" /></p>
<p><img src="https://jeeb.uk/assets/rpi-audio.jpg" alt="rpi audio print" /></p>
<p>The design files for this print can be found on <a href="https://www.thingiverse.com/thing:5248110" target="_blank" rel="noopener noreferrer">Thingiverse</a>.</p>
Raspberry Pi bedside Lamp with Spotify and NodeRed2020-12-02T00:00:00Zhttps://jeeb.uk/posts/bedside-light/<p>Because why wouldn't you want an MQTT controlled, automated lamp with the ability to play music? I can see I don't need to sell you on this idea any more, so let's get right in with what you need:</p>
<h2><a id="requirements" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#requirements" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Requirements</h2>
<ul>
<li><a href="https://shop.pimoroni.com/products/mood-light-pi-zero-w-project-kit" target="_blank" rel="noopener noreferrer">Mood Light - Pi Zero W kit</a> & an SD card</li>
<li>Mini USB powered speakers (I used <a href="https://www.amazon.co.uk/dp/B006RBSHAQ/ref=cm_sw_em_r_mt_dp_U_tTp-EbF2S46NS%5D" target="_blank" rel="noopener noreferrer">this one</a>)</li>
</ul>
<h2><a id="raspberry-pi-setup" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#raspberry-pi-setup" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Raspberry Pi Setup</h2>
<p>Setup the Mood Light as per the instructions.</p>
<p>For the OS choice, you can use pretty much any distribution you want, however I would personally recommend <a href="https://dietpi.com/" target="_blank" rel="noopener noreferrer">DietPi</a>. It has easy installers for the software items below.</p>
<p>As we are using a USB speaker, there are some additional steps you will need to do to make sure that it is the default output for the Pi. I found <a href="https://www.raspberrypi-spy.co.uk/2019/06/using-a-usb-audio-device-with-the-raspberry-pi/" target="_blank" rel="noopener noreferrer">this article</a> very helpful.</p>
<h2><a id="software" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#software" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Software</h2>
<p>If you use DietPi, the below will be easy to install through the <code>dietpi-software</code> tool.</p>
<ul>
<li><a href="https://mopidy.com/" target="_blank" rel="noopener noreferrer">Mopidy</a> will be used to play music through the speaker</li>
<li><a href="https://mopidy.com/ext/spotify/" target="_blank" rel="noopener noreferrer">Mopidy Spotify Extension</a> if you want to use Spotify. Other streaming sources available!</li>
<li><a href="https://nodered.org/" target="_blank" rel="noopener noreferrer">NodeRed</a> will be used to automate the lights and music</li>
<li><a href="https://flows.nodered.org/node/node-red-contrib-mopidy" target="_blank" rel="noopener noreferrer">node-red-contrib-mopidy</a> for a link between NodeRed and Mopidy</li>
<li><a href="https://flows.nodered.org/node/node-red-node-pi-unicorn-hat" target="_blank" rel="noopener noreferrer">node-red-node-pi-unicorn-hat</a> for a link between NodeRed and the lights in the Mood Light</li>
<li><a href="http://mqtt.org/" target="_blank" rel="noopener noreferrer">MQTT</a> will be used to remote control the lamp</li>
</ul>
<p>Make sure the above are all installed and if you are using Spotify, have that setup as well.</p>
<h2><a id="the-automations" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#the-automations" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>The Automations</h2>
<p>Let's deal with turning on the light first. We will setup an automation that looks like the below:</p>
<p><img src="https://jeeb.uk/assets/lamp-automation.png" alt="Lamp Automation" /></p>
<p>Let's walk through that from right to left.</p>
<p>The right-most node is the node from <a href="https://flows.nodered.org/node/node-red-node-pi-unicorn-hat" target="_blank" rel="noopener noreferrer">node-red-node-pi-unicorn-hat</a>. The defaults config for this node should set the brightness to 0%.</p>
<p>The 2 functions set the brightness to 100% brightness:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">payload</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">brightness,100</span><span class="token template-punctuation string">`</span></span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>And set the lamp to a nice yellowish colour:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">return</span> <span class="token punctuation">{</span><br /> <span class="token literal-property property">payload</span><span class="token operator">:</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">255,198,36</span><span class="token template-punctuation string">`</span></span><br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>The left-most node is a simple inject which triggers at 22:00 every day of the week.</p>
<p>Next let's add some some nodes to start playing some music at the same time:</p>
<p><img src="https://jeeb.uk/assets/music-automation.png" alt="Music Automation" /></p>
<p>We have a list of nodes that effect Mopidy:</p>
<ol>
<li><code>tracklist.clear</code> - Remove all the previous tracks in the list in Mopidy</li>
<li><code>mixer.setvolume</code> - I have set this to 10%, but you will need to choose a value best for you</li>
<li><code>tracklist.add</code> - This is where you can choose which tracks to play. I have set the <code>uris</code> value to <a href="https://open.spotify.com/playlist/37i9dQZF1DX9uKNf5jGX6m" target="_blank" rel="noopener noreferrer"><code>["spotify:playlist:37i9dQZF1DX9uKNf5jGX6m"]</code></a></li>
<li><code>playback.play</code> - Play the tracks!</li>
</ol>
<p>I have added a delay for the <code>add</code> and <code>play</code> nodes so that we know that the tracklist has been cleared first and the volume set correctly.</p>
<h3><a id="turning-off-the-light" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#turning-off-the-light" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Turning off the light</h3>
<p>Now that we have turned on the light, we need to turn it off when we want to sleep. We are going to fade out the light for added shazam and continue to play the music for 30 minutes before turning it off. We could do this at a preset time like so:</p>
<p><img src="https://jeeb.uk/assets/turnoff-automation.png" alt="Turnoff Automation" /></p>
<p>The <code>Fade Down Values</code> function outputs an array of values counting down from 100% to 0% in increments of 5:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> startBrightness <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> endBrightness <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> payload <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> startBrightness<span class="token punctuation">;</span> i <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">;</span> i<span class="token operator">-=</span><span class="token number">5</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> payload<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><span class="token keyword">return</span> <span class="token punctuation">{</span><br /> payload<br /><span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>The delay then makes makes sure that each 5% decrement is done once every 1 second.</p>
<p>The bottom part simply waits 30 minutes then pauses the playback.</p>
<p>But that isn't quite good enough. We might want to turn off the lamp before or after a set time.</p>
<h3><a id="remote-control" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#remote-control" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Remote Control</h3>
<p>We will be using MQTT and an Android App called <a href="https://play.google.com/store/apps/details?id=snr.lab.iotmqttpanel.prod&hl=en_GB" target="_blank" rel="noopener noreferrer">IoT MQTT Panel</a> to turn off the lamp when we are ready.</p>
<p>So first we need to add an MQTT node and replace the timing injection node:</p>
<p><img src="https://jeeb.uk/assets/bedside-light/turnoff-remote.png" alt="Turnoff Remote Control" /></p>
<p>And then in IoT MQTT Panel we can set up some controls for the lamp:</p>
<p><img src="https://jeeb.uk/assets/bedside-light/iotmqttpanel.jpg" alt="iotmqttpanel" /></p>
<p>The button with the bed on it publishes to the <code>home/sleep</code> topic. And there are a few other controls in there for manually setting the lamp's colour and brightness. Here is what the nodes look like for dealing with brightness and colour topics:</p>
<p><img src="https://jeeb.uk/assets/bedside-light/mqtt-controls.png" alt="MQTT Controls" /></p>
<h3><a id="conclusion" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bedside-light/#conclusion" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Conclusion</h3>
<p>And we are done! The light and music will start at 22:00 every day. The light will fade out when you click the button in the Android App and the music will stop 30 minutes later. The whole flow can be <a href="https://jeeb.uk/posts/assets/bedside-light/flows.json">found here</a>.</p>
<p>I have some thoughts on how to extend this:</p>
<ul>
<li>(Easy) The music could be faded out rather than abruptly stop</li>
<li>(Average) The setup could be used as an alarm clock with the light and music slowly fading up at a specified time</li>
<li>(Hard) A physical button would be better than using an Android App for turning off the light. Unfortunately there are no GPIO pins we can use. Potentially we could use a USB hub to connect the speaker and some other input.</li>
</ul>
Typescript File Inheritance2020-08-28T00:00:00Zhttps://jeeb.uk/posts/typescript-file-inheritence/<p>I am not sure how useful this pattern is, but it has some interesting effects, especially when sharing code between projects. So let me give you a brief description of it and you can see where to apply it.</p>
<p>Imagine we are writing a simple Web App with a frontend, written in Typescript, and a Node backend, also written in Typescript. This is an excellent opportunity to share some code between the 2 projects. Let's say this simple webapp has a <code>User</code> entity that is passed back and forth between the frontend and the backend, and that the backend stores the <code>User</code> in a database. The database entry might have some additional properties, like <code>password</code> which we don't want to share with the frontend.</p>
<p>First, we are going to create a shared representation of a <code>RequestUser</code>. This interface will be used by both the front-end and the backend and it in a top-level folder called <code>shared</code>:</p>
<p><code>shared/user.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">interface</span> <span class="token class-name">RequestUser</span> <span class="token punctuation">{</span><br /> name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>Then, in the <code>api</code> folder, we want to use the <code>RequestUser</code> interface, but add to it to store more items in the Database entry:</p>
<p><code>api/user.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> User <span class="token keyword">from</span> <span class="token string">"../shared/user.ts"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">interface</span> <span class="token class-name">DatabaseUser</span> <span class="token keyword">extends</span> <span class="token class-name">RequestUser</span> <span class="token punctuation">{</span><br /> password<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>This all seems fine, but what happens when we want to use both <code>RequestUser</code> and <code>DatabaseUser</code> interfaces in our API handler, keeping in mind the <a href="https://james.cx/typescript/2020/06/27/typescript-star-as.html" target="_blank" rel="noopener noreferrer"><code>import * as ...</code></a> patten previously described?</p>
<p><code>api/handler.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> User <span class="token keyword">from</span> <span class="token string">"../shared/user.ts"</span><span class="token punctuation">;</span><br /><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> ApiUser <span class="token keyword">from</span> <span class="token string">"./user.ts"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">handler</span><span class="token punctuation">(</span>body<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> rqUser<span class="token operator">:</span> User<span class="token punctuation">.</span>RequestUser <span class="token operator">=</span> body<span class="token punctuation">.</span>user <span class="token keyword">as</span> User<span class="token punctuation">.</span>RequestUser<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> dbUser<span class="token operator">:</span> ApiUser<span class="token punctuation">.</span>DatabaseUser <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span>rqUser<span class="token punctuation">,</span><br /> <span class="token string-property property">"password"</span><span class="token operator">:</span> <span class="token string">'P@55word'</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>We now have 2 name spaces, <code>ApiUser</code> and <code>User</code> to represent User type models. We can compress that down to 1 namespace, using the <code>export * from</code> pattern. If we look again at our API-specific model file, we can tell it to export everything from the shared model, essentially inheriting all the exported members from that file:</p>
<p><code>api/user.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> User <span class="token keyword">from</span> <span class="token string">"../shared/user.ts"</span><span class="token punctuation">;</span><br /><span class="token keyword">export</span> <span class="token operator">*</span> <span class="token keyword">from</span> <span class="token string">"../shared/user.ts"</span><span class="token punctuation">;</span><br /><span class="token operator">...</span></code></pre>
<p>And then in our handler again, we can clear up some of the references:</p>
<p><code>api/handler.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> User <span class="token keyword">from</span> <span class="token string">"./user.ts"</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">handler</span><span class="token punctuation">(</span>body<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> rqUser<span class="token operator">:</span> User<span class="token punctuation">.</span>RequestUser <span class="token operator">=</span> body<span class="token punctuation">.</span>user <span class="token keyword">as</span> User<span class="token punctuation">.</span>RequestUser<span class="token punctuation">;</span><br /> <span class="token keyword">const</span> dbUser<span class="token operator">:</span> User<span class="token punctuation">.</span>DatabaseUser <span class="token operator">=</span> <span class="token punctuation">{</span><br /> <span class="token operator">...</span>rqUser<span class="token punctuation">,</span><br /> <span class="token string-property property">"password"</span><span class="token operator">:</span> <span class="token string">'P@55word'</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>We have one less import, and we can see that the <code>RequestUser</code> and <code>DatabaseUser</code> are very closely related as they both come from the same namespace. It is slightly cleaner code, and lets newcomers to our code read and scan it with ease.</p>
Using BitWarden and Chezmoi to manage SSH keys2020-08-13T00:00:00Zhttps://jeeb.uk/posts/bitwarden-chezmoi-ssh-key/<p>I have recently started using <a href="https://www.chezmoi.io/" target="_blank" rel="noopener noreferrer">Chezmoi</a> to manage my dotfiles (and various other pieces software config) across multiple machines. The distribution is done via a git repo and therefore we should not check in secrets such as the private part of the SSH key. Using <a href="https://bitwarden.com/" target="_blank" rel="noopener noreferrer">Bitwarden</a>, we can store the key in a Secure Note and retrieve on the other machines.</p>
<h2><a id="setup" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bitwarden-chezmoi-ssh-key/#setup" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Setup</h2>
<p>The rest of this post assumes you already have <a href="https://www.chezmoi.io/" target="_blank" rel="noopener noreferrer">Chezmoi</a> installed and set up:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">curl</span> -sfL https://git.io/chezmoi <span class="token operator">|</span> <span class="token function">sh</span><br />chezmoi init</code></pre>
<p>You will also need a pre-existing SSH key:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">ssh_keygen -o</code></pre>
<h2><a id="store-the-key" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bitwarden-chezmoi-ssh-key/#store-the-key" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Store the key</h2>
<p>The public key part of the SSH key can be stored in Chezmoi in plain text:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">chezmoi <span class="token function">add</span> .ssh/id_rsa.pub</code></pre>
<p>To store the private part we are going to need to install the <a href="https://github.com/bitwarden/cli" target="_blank" rel="noopener noreferrer"><code>bitwarden-cli</code></a> and then login and unlock it:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">bw login <span class="token operator"><</span>EMAIL-ADDRESS<span class="token operator">></span><br />bw unlock<br /><span class="token builtin class-name">export</span> <span class="token assign-left variable">BW_SESSION</span><span class="token operator">=</span><span class="token string">"<SESSION-ID>"</span></code></pre>
<p>Now, we get to the magic sauce. This line will store your SSH key (stored at <code>~/.ssh/id_rsa</code>) in a secure note in Bitwarden:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"{<span class="token entity" title="\"">\"</span>organizationId<span class="token entity" title="\"">\"</span>:null,<span class="token entity" title="\"">\"</span>folderId<span class="token entity" title="\"">\"</span>:null,<span class="token entity" title="\"">\"</span>type<span class="token entity" title="\"">\"</span>:2,<span class="token entity" title="\"">\"</span>name<span class="token entity" title="\"">\"</span>:<span class="token entity" title="\"">\"</span>sshkey<span class="token entity" title="\"">\"</span>,<span class="token entity" title="\"">\"</span>notes<span class="token entity" title="\"">\"</span>:<span class="token entity" title="\"">\"</span><span class="token variable"><span class="token variable">$(</span><span class="token function">sed</span> -e <span class="token string">':a'</span> -e <span class="token string">'N'</span> -e <span class="token string">'$!ba'</span> -e <span class="token string">'s/\n/\\\\n/g'</span> ~/.ssh/id_rsa<span class="token variable">)</span></span><span class="token entity" title="\"">\"</span>,<span class="token entity" title="\"">\"</span>favorite<span class="token entity" title="\"">\"</span>:false,<span class="token entity" title="\"">\"</span>fields<span class="token entity" title="\"">\"</span>:[],<span class="token entity" title="\"">\"</span>login<span class="token entity" title="\"">\"</span>:null,<span class="token entity" title="\"">\"</span>secureNote<span class="token entity" title="\"">\"</span>:{<span class="token entity" title="\"">\"</span>type<span class="token entity" title="\"">\"</span>:0},<span class="token entity" title="\"">\"</span>card<span class="token entity" title="\"">\"</span>:null,<span class="token entity" title="\"">\"</span>identity<span class="token entity" title="\"">\"</span>:null}"</span> <span class="token operator">|</span> bw encode <span class="token operator">|</span> bw create item</code></pre>
<p>And finally, we need to tell chezmoi where to get the key from. Create a file in your chezmoi repo at this location: <code>private_dot_ssh/private_id_rsa.tmpl</code> and add this as the contents:</p>
<pre><code>{{ (bitwarden "item" "sshkey").notes }}
</code></pre>
<p>(For OSX, this file needs a new line character at the end. For Linux, I believe it mustn't, so you might need to end the file with <code>-}}</code> instead)</p>
<p>Make sure all the files are committed and pushed to the origin.</p>
<h2><a id="retrieve-the-key" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/bitwarden-chezmoi-ssh-key/#retrieve-the-key" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Retrieve the key</h2>
<p>On another machine where you want to retrieve the same key, make sure <code>bitwarden-cli</code> and Chezmoi are installed and first do the same login and unlock steps for Bitwarden as above. Then simplpy do:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">chezmoi init --apply <span class="token operator"><</span>GIT-REPO<span class="token operator">></span></code></pre>
<p>And that's it. Check your private key has made it safely to your machine by doing <code>cat ~/.ssh/id_rsa</code>.</p>
<p>You can see the full example of my chezmoi config <a href="https://github.com/jmc265/dotfiles" target="_blank" rel="noopener noreferrer">here</a>.</p>
Typescript Multi Async Interface2020-08-03T00:00:00Zhttps://jeeb.uk/posts/typescript-multi-async/<p>I recently had to write a Javascript library which was going to be used in multiple different contexts by lots of different people. The purpose of the library was to supply a stream of updating values. Internally to the library, <a href="https://github.com/ReactiveX/rxjs" target="_blank" rel="noopener noreferrer">RxJS</a> is used as it excellently models streams of updating values. However, the interface to the library should not expose the inner workings.</p>
<p>So, I wanted to allow the consumer of the library to access the values in a number of ways:</p>
<h2><a id="usage" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/typescript-multi-async/#usage" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Usage</h2>
<h3><a id="promises" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/typescript-multi-async/#promises" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Promises</h3>
<p>The standard way of supplying asynchronous values. It is commonly used and with <code>async/await</code> the code is very readable:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">const</span> currentValue <span class="token operator">=</span> <span class="token keyword">await</span> lib<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The problem with this is, you can only get one value, you won't receive any further updates to the value. So, we can also support the next access method:</p>
<h3><a id="event-emitter" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/typescript-multi-async/#event-emitter" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Event Emitter</h3>
<p>This will allow the consumer to subscribe to updates in the value and the syntax would be as follows:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">let</span> currentValue<span class="token punctuation">;</span><br />lib<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">onNew</span><span class="token punctuation">(</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span> <span class="token operator">=></span> currentValue <span class="token operator">=</span> value<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h3><a id="synchronous" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/typescript-multi-async/#synchronous" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Synchronous</h3>
<p>I also wanted to allow the users of the library to synchronously obtain the latest as well as all previous values when they were unable to use async or event emitters:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">const</span> currentValue <span class="token operator">=</span> lib<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">latest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> previousValues <span class="token operator">=</span> lib<span class="token punctuation">.</span><span class="token function">getValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">all</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<h2><a id="implementation" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/typescript-multi-async/#implementation" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Implementation</h2>
<p>We can see above that the <code>getValue()</code> function has some interesting properties. It returns something which can be listened to like a promise, but it also returns an object which has other methods on it for accessing the values. So what does this object look like? Well here is a Typescript interface describing the object that <code>getValue()</code> returns:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">interface</span> <span class="token class-name">AsyncAccessor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span></span> <span class="token keyword">extends</span> <span class="token class-name">PromiseLike<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span></span> <span class="token punctuation">{</span><br /> <span class="token function">onNew</span><span class="token punctuation">(</span><span class="token function-variable function">listener</span><span class="token operator">:</span> <span class="token punctuation">(</span>value<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">void</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token keyword">void</span><span class="token punctuation">;</span><br /> <span class="token function">latest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">;</span><br /> <span class="token function">all</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>And using RxJS, the implementation of this class is fairly simple:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">class</span> <span class="token class-name">ValueAccessor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span></span> <span class="token keyword">implements</span> <span class="token class-name">AsyncAccessor<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span></span> <span class="token punctuation">{</span><br /> <span class="token keyword">private</span> _previousValues<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /><br /> <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token keyword">private</span> _valueStream<span class="token operator">:</span> Observable<span class="token operator"><</span><span class="token constant">T</span><span class="token operator">></span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> _valueSteam<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>v <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>_previousValues<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>v<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token function">getLatest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token constant">T</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_previousValues<span class="token punctuation">[</span><span class="token keyword">this</span><span class="token punctuation">.</span>_previousValues<span class="token punctuation">.</span>length<span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token function">getAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_previousValues<span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token function">onNew</span><span class="token punctuation">(</span><span class="token function-variable function">listener</span><span class="token operator">:</span> <span class="token punctuation">(</span>value<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">void</span><span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">this</span><span class="token punctuation">.</span>_valueSteam<span class="token punctuation">.</span><span class="token function">subscribe</span><span class="token punctuation">(</span>value <span class="token operator">=></span> <span class="token function">listener</span><span class="token punctuation">(</span>value<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><br /> <span class="token keyword">public</span> <span class="token generic-function"><span class="token function">then</span><span class="token generic class-name"><span class="token operator"><</span>TResult1 <span class="token operator">=</span> <span class="token constant">T</span><span class="token punctuation">,</span> TResult2 <span class="token operator">=</span> <span class="token builtin">never</span><span class="token operator">></span></span></span><span class="token punctuation">(</span>onfulfilled<span class="token operator">?</span><span class="token operator">:</span> <span class="token punctuation">(</span>value<span class="token operator">:</span> <span class="token constant">T</span><span class="token punctuation">)</span> <span class="token operator">=></span> TResult1 <span class="token operator">|</span> PromiseLike<span class="token operator"><</span>TResult1<span class="token operator">></span><span class="token punctuation">,</span> onrejected<span class="token operator">?</span><span class="token operator">:</span> <span class="token punctuation">(</span>reason<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span> <span class="token operator">=></span> TResult2 <span class="token operator">|</span> PromiseLike<span class="token operator"><</span>TResult2<span class="token operator">></span><span class="token punctuation">)</span><span class="token operator">:</span> PromiseLike<span class="token operator"><</span>TResult1 <span class="token operator">|</span> TResult2<span class="token operator">></span> <span class="token punctuation">{</span><br /> <span class="token keyword">const</span> previousValue <span class="token operator">=</span> <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">getLatest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token keyword">if</span> <span class="token punctuation">(</span>previousValue<span class="token punctuation">)</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token builtin">Promise</span><span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span>previousValue<span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>onfulfilled<span class="token punctuation">,</span> onrejected<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /> <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>_valueSteam<br /> <span class="token punctuation">.</span><span class="token function">pipe</span><span class="token punctuation">(</span><span class="token function">first</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">toPromise</span><span class="token punctuation">(</span><span class="token punctuation">)</span><br /> <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span>onfulfilled<span class="token punctuation">,</span> onrejected<span class="token punctuation">)</span><span class="token punctuation">;</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span></code></pre>
<p>And that's it. We now have a value accessor that can be used in a number of different values by a consumer to the library.</p>
Typescript `import * as ...`2020-06-27T00:00:00Zhttps://jeeb.uk/posts/typescript-star-as/<p>There is a very easy to use and simple pattern that can be used whilst importing files in Typescript. The main advantage of using this pattern is to increase the readability and scan-ability of code. And that pattern is <code>import * as ...</code></p>
<p>It is especially useful when it is used with models, so let's have a look at a very simple model:</p>
<p><code>user.ts</code></p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">interface</span> <span class="token class-name">UserType</span> <span class="token punctuation">{</span><br /> firstName<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /> surname<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">fromJson</span><span class="token punctuation">(</span>input<span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">)</span><span class="token operator">:</span> UserType <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token punctuation">{</span><br /> firstName<span class="token operator">:</span> input<span class="token punctuation">.</span>firstName <span class="token operator">||</span> <span class="token string">""</span><span class="token punctuation">,</span><br /> surname<span class="token operator">:</span> input<span class="token punctuation">.</span>surname <span class="token operator">||</span> <span class="token string">""</span><br /> <span class="token punctuation">}</span><br /><span class="token punctuation">}</span><br /><br /><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">getName</span><span class="token punctuation">(</span>user<span class="token operator">:</span> UserType<span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">{</span><br /> <span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>user<span class="token punctuation">.</span>firstName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>user<span class="token punctuation">.</span>surname<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span><br /><span class="token punctuation">}</span></code></pre>
<p>If we don't use the <code>import *</code> syntax, we would have to import the 2 functions separately as follows:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token punctuation">{</span>fromJson<span class="token punctuation">,</span> getName<span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'user.ts'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> input <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">/* some JSON */</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /><span class="token keyword">const</span> object <span class="token operator">=</span> <span class="token function">fromJson</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token function">getName</span><span class="token punctuation">(</span>object<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The above is not particularly easily to scan. We get to the usage of <code>fromJson</code>, but as the reader we have to break our scan by either looking at the top of file to see where the function came from or use our IDE's intellisense to see what the type of <code>object</code> is. Furthermore, on the next line, the function name <code>getName</code> might not remain unique to <code>User</code>.</p>
<p>So, in order to make the code easier to scan and parse for others, we can user the <code>import * as</code> pattern:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">import</span> <span class="token operator">*</span> <span class="token keyword">as</span> User <span class="token keyword">from</span> <span class="token string">'user.ts'</span><span class="token punctuation">;</span><br /><br /><span class="token keyword">const</span> input <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token comment">/* some JSON */</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><br /><span class="token keyword">const</span> object <span class="token operator">=</span> User<span class="token punctuation">.</span><span class="token function">fromJson</span><span class="token punctuation">(</span>input<span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>User<span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span>object<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>From scanning this file, the reader can very quickly understand that the variable <code>object</code> is a <code>User</code> because the function <code>fromJson</code> is namespaced to that file. The <code>getName</code> call can now be disambiguated from a <code>getName</code> function that might exist on another type of object.</p>
<p>This pattern also works nicely for utils or helpers, as such:</p>
<pre class="language-typescript" tabindex="0"><code class="language-typescript"><span class="token keyword">const</span> user <span class="token operator">=</span> db<span class="token punctuation">.</span><span class="token function">getUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> user <span class="token operator">=</span> cache<span class="token punctuation">.</span><span class="token function">getUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><br /><span class="token keyword">const</span> user <span class="token operator">=</span> queue<span class="token punctuation">.</span><span class="token function">getUser</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
3D modelled and printed roof2020-04-04T00:00:00Zhttps://jeeb.uk/posts/3d-printed-roof/<p>As part of renovations on our house, we wanted to explore the possibility of extending into the loft. Unfortunately the roof on our house was an odd shape with many different parts and ridges.</p>
<p>When trying to consider how best to go about building in the loft, I wanted to model the existing layout of the roof as well a possibilities for extending. I could then use these designs when talking to the builders to show what we wanted.</p>
<p>Lucky, we had accurate sketches of the geometry of the house already when other work was done on our house. I spent some time modelling this in <a href="https://app.sketchup.com/" target="_blank" rel="noopener noreferrer">SketchUp</a>:</p>
<p><img src="https://jeeb.uk/assets/3droof.png" alt="3D roof design" /></p>
<p>And then simply print it out with my <a href="https://www.monoprice.uk/products/monoprice-select-mini-v2-3d-printer-with-heated-build-plate" target="_blank" rel="noopener noreferrer">Monoprice Mini V2</a>:</p>
<p><img src="https://jeeb.uk/assets/3droof.jpg" alt="3D roof print" /><br />
(Please forgive the rough edges, I didn't spend any time finishing it)</p>
<p>After conferring with a few different builders, using the model to better communicate what we had in mind, we decided not to proceed with any extension as it would be too complex and costly.</p>
Color Explosion Unity game2016-03-19T00:00:00Zhttps://jeeb.uk/posts/color-explosion-unity-game/<p><img src="https://jeeb.uk/assets/colour-explosion.PNG" alt="" /></p>
<p>In 2016 I became interested in learning how to create games. My explorations into this previously had been fairly tame, creating a text-based adventure game in <a href="https://en.wikipedia.org/wiki/QBasic" target="_blank" rel="noopener noreferrer">QBasic</a> when I was around 10 and then a Java GUI dungeon-crawler type game in university for some coursework. In 2016, Unity was free and used C# as the language of choice, which made it perfect for me. I watched some online tutorials on how to make games and got started!</p>
<p>I quickly realised that although I had the programming knowledge on how to create a game, what I lacked was the graphical ability to create assets. Instead of purchasing a pack of assets, or attempting to gather what I could from free resources, I decided to make the actual visuals for the game as simple as possible, using graphical primitives like squares and circles (the game was to be 2D for ease on my first attempt).</p>
<p>I landed on the idea of shapes entering the screen from the left and moving towards the right. The player was to tap on the shape before it exited the screen on the right. Simple enough. I wanted to add a bit of complexity so there are different game modes, and also I wanted to make it look stylised, so the shapes had a certain colour palette and glow and would explode in retro but futuristic ways when hit.</p>
<p>The result was <a href="https://play.google.com/store/apps/details?id=uk.japplications.colorExp" target="_blank" rel="noopener noreferrer">Color Explosion</a>.</p>
2 very early Material Design Android apps2014-05-11T00:00:00Zhttps://jeeb.uk/posts/j-applications/<p>In 2014, Google announced a new design language, called <a href="https://material.io/design" target="_blank" rel="noopener noreferrer">Material Design</a>, which it put forward as the newest iteration of standard for interaction on mobile devices. At the time, it was in a very early form, the details were quite sparse and there were no apps "in the wild" which used it. My plan was to follow the standards and rules as closely as possible and create some very basic apps and release them. The 2 apps that I ended up creating are detailed below:</p>
<h1><a id="jtodo" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/j-applications/#jtodo" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>J.ToDo</h1>
<p><img src="https://jeeb.uk/assets/japplications/jtodo-hero.PNG" alt="J.ToDo" /><br />
<img src="https://jeeb.uk/assets/japplications/jtodo-details.PNG" alt="" /></p>
<p>The J.ToDo app has over 10k installs to date, and despite it being over 8 years old at time of writing (first released 11 May 2014), there are still 204 active installs on devices. The average rating is 4.2 from 467 reviews.</p>
<p><a href="https://play.google.com/store/apps/details?id=uk.japplications.jtodo" target="_blank" rel="noopener noreferrer">Play store Listing</a></p>
<h1><a id="jnotepad" class="heading-anchor apply-svg-filter" href="https://jeeb.uk/posts/j-applications/#jnotepad" aria-hidden="true"><svg aria-hidden="true" class="octicon octicon-link" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>J.Notepad</h1>
<p><img src="https://jeeb.uk/assets/japplications/jnotepad-hero.PNG" alt="JNotepad" /><br />
<img src="https://jeeb.uk/assets/japplications/jnotepad-details.PNG" alt="JNotepad details" /></p>
<p>The J.Notepad app was released a year after the ToDo app and never did as well as my first one. It has over 5k installs in total and is still used on 117 device. It has a rating of 3.9 from 171 reviews.</p>
<p><a href="https://play.google.com/store/apps/details?id=uk.japplications.jnotepad" target="_blank" rel="noopener noreferrer">Play Store Listing</a></p>
My first web site in 20012001-09-22T00:00:00Zhttps://jeeb.uk/posts/first-site/<p>Back in 2001, when I was 11 years old, my dad bought a website domain for me to use. According to the <a href="https://archive.org/web/" target="_blank" rel="noopener noreferrer">Wayback Machine</a> this site lasted from Sep 2001 to Jan 2004 and it looks exactly as you might expect an 11 year old to design a site at the turn of the millennium.</p>
<p>The <code>index.html</code> features the most rare of HTML artefacts these days: the horizontal scrolling marquee.<br />
<img src="https://jeeb.uk/assets/first-site/home.png" alt="homepage" /><br />
There was no PHP or CGI, no Javascript. Not even any CSS. We are just talking bare HTML. Also present is a lovely bit of clipart that I believe came straight from Microsoft Office at the time. Interestingly, you can see an advert banner at the top which was inserted on to the page by the domain registrar that we used at the time: <a href="https://www.easyspace.com/" target="_blank" rel="noopener noreferrer">Easyspace</a>.</p>
<p>Each member of the family got their own page!</p>
<p><img src="https://jeeb.uk/assets/first-site/mum.png" alt="mum" /><br />
<img src="https://jeeb.uk/assets/first-site/dad.png" alt="dad" /><br />
<img src="https://jeeb.uk/assets/first-site/emily.png" alt="emily" /><br />
<img src="https://jeeb.uk/assets/first-site/me.png" alt="me" /></p>
<p>Looking through the source for the page, we can see that it was hosted on user space that our ISP at the time gave us: <code>http://www.users.totalise.co.uk/~first.last/</code></p>
<p>Also in the HTML are the tell-tale signs of the IDE used to create the site. <a href="https://en.wikipedia.org/wiki/Microsoft_FrontPage" target="_blank" rel="noopener noreferrer">Microsoft Frontpage</a> was my first foray into web site creation (swiftly followed by <a href="https://en.wikipedia.org/wiki/Adobe_Dreamweaver" target="_blank" rel="noopener noreferrer">Macromedia Dreamweaver</a>):</p>
<p><img src="https://jeeb.uk/assets/frontpage.png" alt="frontpage meta" /></p>