<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Rein van &apos;t Veer</title>
    <description>Rein van &apos;t Veer, geospatial and semantic web developer. All content copyright Rein van &apos;t Veer CC-BY-SA
</description>
    <link>https://reinvantveer.github.io/</link>
    <atom:link href="https://reinvantveer.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Mon, 18 May 2026 10:49:23 +0000</pubDate>
    <lastBuildDate>Mon, 18 May 2026 10:49:23 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>A Generative Life Drawing Model</title>
        <description>&lt;h2 id=&quot;on-how-ai-can-be-of-use-to-artists&quot;&gt;On how AI can be of use to artists&lt;/h2&gt;

&lt;p&gt;The word “model” has to be one of the most overloaded terms in the history of human language. An abstraction or 
physical thing, tangible or ephemeral, something that stands for something else but can be something of itself at the 
same time. In this article I will combine “models” of both kinds, from the artistic domain and the “artificial 
intelligence” domain, although I try to shy away from this presumptious term, I’d rather opt for “machine learning” in
most cases.&lt;/p&gt;

&lt;div style=&quot;display: table&quot;&gt;
    &lt;figure style=&quot;width: 40%; float: left&quot;&gt;
      &lt;img src=&quot;/images/models/model-g2f005866f_1920.jpg&quot; alt=&quot;An artist&apos;s &apos;model&apos;? Or the real thing?&quot; /&gt;
      &lt;figcaption&gt;An artist&apos;s &quot;model&quot;? Or the real thing? Source: 
        &lt;a href=&quot;https://pixabay.com/photos/model-redhead-education-5953621/&quot;&gt;Pixabay, by Victory Borodinova&lt;/a&gt;
      &lt;/figcaption&gt;
    &lt;/figure&gt;
    
    &lt;figure style=&quot;width: 40%; float: right&quot;&gt;
      &lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/1925_Ford_Model_T_at_Hatfield_Heath_Festival_2017_-_01.jpg/450px-1925_Ford_Model_T_at_Hatfield_Heath_Festival_2017_-_01.jpg?20170710083231&quot; alt=&quot;The Ford &apos;model&apos; T&quot; /&gt;
      &lt;figcaption&gt;The Ford &quot;model&quot; T. Yep, a model too. Source: 
        &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:1925_Ford_Model_T_at_Hatfield_Heath_Festival_2017_-_01.jpg&quot;&gt;CC-BY-SA 4 WikiMedia Commons, by Acabashi&lt;/a&gt;
      &lt;/figcaption&gt;
    &lt;/figure&gt;
&lt;/div&gt;

&lt;h3 id=&quot;art-models&quot;&gt;Art models&lt;/h3&gt;

&lt;p&gt;The artistic context: a model (in the stricter sense) is usually a person. Someone offering to pose for artists such a
sculptors or painters. The model acts as a reference for the artist to draw or sculpt a work of art. There is, of
course, something weird about this. The “model” in this case, the actual thing. The artwork could be more aptly
described as a model for the real thing, rather than the other way round!&lt;/p&gt;

&lt;h3 id=&quot;machine-learning-models&quot;&gt;Machine learning models&lt;/h3&gt;

&lt;p&gt;Machine learning models usually &lt;em&gt;are&lt;/em&gt; proxies for a real (or an imagined) world domain. “Classic” domain examples, in as
far as anything in a field so young can be called as such, are models for
the &lt;a href=&quot;http://yann.lecun.com/exdb/mnist/&quot;&gt;“MNIST” handwritten digit recognition&lt;/a&gt; or
the &lt;a href=&quot;https://www.image-net.org/challenges/LSVRC/&quot;&gt;“ILSVRC” image recognition challenge&lt;/a&gt;. These datasets have played a
significant role in shaping machine learning history. The “models” in the machine learning case, are pieces of software
that are able to perform some task or other: recognizing digits, or classifying images.&lt;/p&gt;

&lt;h2 id=&quot;models-generating-models&quot;&gt;Models generating models&lt;/h2&gt;

&lt;p&gt;Machine learning and art share a great deal of shared interest. There are numerous examples of cases where machine 
learning has been used to generate or emulate art, often with great success.&lt;/p&gt;

&lt;p&gt;In my &lt;a href=&quot;/2022/01/29/easier-operator.html&quot;&gt;previous post&lt;/a&gt; I used a generative model to generate images from text to
include in my article. This was more something of a gimmick, to spice up the article a bit and to play around. But it
sparked my interest again in using generative models for artistic purposes. Especially since I started sharing an 
atelier with seven other art painters.&lt;/p&gt;

&lt;h2 id=&quot;fafa-vae&quot;&gt;FAFA-VAE&lt;/h2&gt;

&lt;h3 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h3&gt;

&lt;p&gt;The first iteration of my model created logs like this:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; 1/16 [&amp;gt;.............................] - ETA: 51s - loss: 283867.6875 - reconstruction_loss: 283867.4375 - kl_loss: 0.2540
 2/16 [==&amp;gt;...........................] - ETA: 22s - loss: inf - reconstruction_loss: 70986436616009023488.0000 - kl_loss: inf
 3/16 [====&amp;gt;.........................] - ETA: 20s - loss: nan - reconstruction_loss: nan - kl_loss: nan
 4/16 [======&amp;gt;.......................] - ETA: 22s - loss: nan - reconstruction_loss: nan - kl_loss: nan
 5/16 [========&amp;gt;.....................] - ETA: 19s - loss: nan - reconstruction_loss: nan - kl_loss: nan
 6/16 [==========&amp;gt;...................] - ETA: 16s - loss: nan - reconstruction_loss: nan - kl_loss: nan
 7/16 [============&amp;gt;.................] - ETA: 14s - loss: nan - reconstruction_loss: nan - kl_loss: nan
 ... (etc)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;When this happens, you know there is something seriously wrong with the training. It could happen randomly, but in my
case, it was consistent. Deep learning is a much more a matter of “negotiation” with a model, data and a loss function
than “normal” programming is. At times it really feels like you have to “coach” your model into training properly.&lt;/p&gt;

&lt;p&gt;So what do these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nan&lt;/code&gt;s mean? &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nan&lt;/code&gt; is, of course “not a number”, which happens whenever your model tries to:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;divide by zero&lt;/li&gt;
  &lt;li&gt;tries to compute log(0)&lt;/li&gt;
  &lt;li&gt;tries to take the root of a negative number&lt;/li&gt;
  &lt;li&gt;all the things they taught you that you shouldn’t do with numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we look closely, we see that the model doesn’t start out with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nan&lt;/code&gt; loss. It actually computes something sensible,
but right after epoch 1, it spirals out of control. The reconstruction loss explodes and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kl_loss&lt;/code&gt;, which is the 
&lt;a href=&quot;https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence&quot;&gt;Kullback-Leibler divergence&lt;/a&gt; loss, is so large that
it overflows the max value and becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inf&lt;/code&gt;. This is generally caused by a phenomenon known 
as &lt;a href=&quot;https://machinelearningmastery.com/exploding-gradients-in-neural-networks/&quot;&gt;“exploding gradients”&lt;/a&gt;. The trouble is
that there are many known causes, but fortunately also many known fixes for it. It is also an inescapable part of
machine learning model development.&lt;/p&gt;

&lt;p&gt;Things that may help:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Normalizing/whitening of your input data. Deep neural nets generally need data with mean 0 and unit variance. In my 
case, I already used either feature-wise normalization or sample-wise normalisation, but no whitening.&lt;/li&gt;
  &lt;li&gt;Lowering the learning rate. In my case, this helped to debug things. At the very least it allows you to see how the
model losses develop.&lt;/li&gt;
  &lt;li&gt;Fix your loss function. Maybe there’s a mathematical error or a bug in there somewhere. Look closely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, I switched from a binary cross-entropy to a mean squared error to calculate the image reconstruction loss.
Why this helps in the first place is still beyond me. The reasons to choose either one of these loss functions is quite
complex.&lt;/p&gt;
</description>
        <pubDate>Sat, 26 Mar 2022 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2022/03/26/a-generative-life-drawing-model.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2022/03/26/a-generative-life-drawing-model.html</guid>
        
        
      </item>
    
      <item>
        <title>The easier way to K8s Operator development using Argo Events</title>
        <description>&lt;h2 id=&quot;on-why-you-dont-need-special-operator-sdks-to-develop-k8s-operators&quot;&gt;On why you don’t need special operator SDKs to develop K8s Operators&lt;/h2&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/generated-k8s-operator-using-argo-events.png&quot; alt=&quot;Kubernetes Operator using Argo Events. An AI impression.&quot; /&gt;
  &lt;figcaption&gt;&quot;Kubernetes Operator using Argo Events&quot;. An AI impression. Source: 
    &lt;a href=&quot;https://vision-explorer.allenai.org/text_to_image_generation&quot;&gt;Allen Institute for AI Computer Vision Explorer&lt;/a&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;tldr-or-the-management-summary-if-you-like&quot;&gt;TL;DR (or the “management summary” if you like)&lt;/h2&gt;

&lt;p&gt;If your organisation uses Kubernetes, chances are that you’re using 
&lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/operator/&quot;&gt;operators&lt;/a&gt;. Once you get to the point that you want to
develop your own operators, there comes the choice in how to develop them. Most people go with one of the major operator
frameworks, but this isn’t necessarily the right track if you just start out: many frameworks are bulky and hard to set
up. What I found out, is that using &lt;a href=&quot;https://argoproj.github.io/argo-events/&quot;&gt;Argo Events&lt;/a&gt; 
and &lt;a href=&quot;https://argoproj.github.io/argo-workflows/&quot;&gt;Argo Workflows&lt;/a&gt;, you can&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;have fully functional operators using just a handful of files (manifests);&lt;/li&gt;
  &lt;li&gt;have your operator without the need for publishing custom-built Docker images;&lt;/li&gt;
  &lt;li&gt;have much, much improved inspection into your operators’ handling of resources thanks to the Argo UI.&lt;/li&gt;
  &lt;li&gt;develop and test the operator logic in the Argo workflow template &lt;em&gt;independently&lt;/em&gt; from the operator itself, 
using the Argo UI;&lt;/li&gt;
  &lt;li&gt;use &lt;strong&gt;&lt;em&gt;any&lt;/em&gt;&lt;/strong&gt; programming language you like, not just the ones supported by the “official” frameworks!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;however&quot;&gt;However&lt;/h3&gt;

&lt;p&gt;The example given below is set up as a tutorial. Make sure you don’t simply copy-paste it into production: let your
dev team set things up properly and have your infrastructure security advisor go through it. The tutorial below is just
for instruction purposes. It is a serious proposal though. You can have fully functional operators without pushing a
single Docker image, or without using a single Operator SDK and have a scalable solution. Bear in mind that:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The setup in this tutorial uses quite a few containers, mostly for debugging. This can be compressed into a single
container however, with a bit of refactoring. This, I didn’t put into the tutorial.&lt;/li&gt;
  &lt;li&gt;This method isn’t the fastest. It uses an Argo Workflow, which takes a few seconds to instantiate, for each time you 
create, update or delete a custom resource. For the vast majority of purposes, this is fine. If you really must
scale your operator to deploy hundreds of applications per hour, then this method can still work, but it might not be
the most efficient solution. Then again, few companies need to build an operator from scratch that needs to scale to
this level immediately. For starting out, this method is excellent because it gives you so much more transparency.&lt;/li&gt;
  &lt;li&gt;Operator development is never easy. And with this approach, you will probably need to learn quite a bit of Argo 
domain-specific language. Like most k8s frameworks it’s huge, but it’s useful for learning it in its own right, as
Argo is useful for a &lt;em&gt;lot more than Operator development&lt;/em&gt;. And: it’s very well documented.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;intro-kubernetes-operators&quot;&gt;Intro: Kubernetes Operators&lt;/h2&gt;

&lt;p&gt;In the past few weeks or so, I followed up on an idea I had that had been lingering in my mind for a while. Something
that showed particular promise, but I hadn’t found the time to dig into, until now. I had been involved
in &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/operator/&quot;&gt;Kubernetes Operator&lt;/a&gt; development for a few months
and had the distinct feeling that things could be improved significantly. But before we go into detail, let’s look at
operators for a moment. &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/operator/&quot;&gt;Kubernetes Operators&lt;/a&gt; are
central to doing anything in Kubernetes. Operators are controllers that make sure that
particular &lt;a href=&quot;https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/&quot;&gt;kinds of Kubernetes “things”&lt;/a&gt;, 
be it an application or something else, end up the way you specified them to be.&lt;/p&gt;

&lt;p&gt;In kubernetes speak: operators are responsible for reconciliation of the desired state with the actual state for a
particular
&lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/&quot;&gt;custom resource&lt;/a&gt; type that
extend the Kubenetes API. Kubernetes comes with its own controllers
for &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/deployment/&quot;&gt;Deployments&lt;/a&gt;,
&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/ingress/&quot;&gt;Ingresses&lt;/a&gt;,
&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/&quot;&gt;Pods&lt;/a&gt; and what have you - these you don’t need to build yourself.
But once you want to extend the Kubernetes system with your own types, you get to Operators.&lt;/p&gt;

&lt;h2 id=&quot;why-operators-exist&quot;&gt;Why Operators exist&lt;/h2&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/generated-extending-k8s-api.jpg&quot; alt=&quot;Extending the Kubernetes API, an AI-generated impression&quot; /&gt;
  &lt;figcaption&gt;&quot;Extending the Kubernetes API&quot;, an AI-generated impression. Source:
    &lt;a href=&quot;https://vision-explorer.allenai.org/text_to_image_generation&quot;&gt;DeepAI.org Text To Image API&lt;/a&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;In practice it means that an operator is responsible for the life cycle of a resource type of your own design. For
example a particular database type, say &lt;a href=&quot;https://www.postgresql.org/&quot;&gt;PostgreSQL&lt;/a&gt;, or something else entirely. Of course
there already are perfectly fine &lt;a href=&quot;https://operatorhub.io/operator/postgres-operator&quot;&gt;operators available for PostgreSQL&lt;/a&gt;
, so you don’t need to design your own. The reason this operator exists, is because deploying a production-grade
PostgreSQL database &lt;em&gt;cluster&lt;/em&gt;
including a backup strategy, is far from trivial and you need many, many manifests to explain to Kubernetes how you want
your infrastructure set up. Operators like these help you declare the layout and settings of your PostgreSQL cluster,
set up complex infrastructure, taking most cognitive load off your plate. There are operators that deploy applications,
manage storage, or even interface with something outside the Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Two articles of vital importance if you really want to sink your teeth in:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/operator/&quot;&gt;“Operator pattern” article&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;The &lt;a href=&quot;https://github.com/cncf/tag-app-delivery/blob/main/operator-wg/whitepaper/Operator-WhitePaper_v1-0.md&quot;&gt;“CNCF Operator White Paper”&lt;/a&gt;.
CNCF stands for the &lt;a href=&quot;https://www.cncf.io/&quot;&gt;“Cloud Native Computing Foundation”&lt;/a&gt; and is the main organisation backing the design and development of Kubernetes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important to know is that:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Operators listen to your self-designed “custom resource definitions”, instances of which
are &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/&quot;&gt;“custom resources”&lt;/a&gt;, so
you can extend the Kubernetes API itself.&lt;/li&gt;
  &lt;li&gt;Operators act on create, update and delete events, so they are responsible for the entire life cycle of the resources
they create from your custom resource.&lt;/li&gt;
  &lt;li&gt;Operators should not do any heavy lifting, as they are expected to easily and stably scale. If any resource-heavy work
is involved, this is to be left to other resource definitions, such as Jobs.&lt;/li&gt;
  &lt;li&gt;Operators should be highly available: they’re not supposed to drop the ball and should reliably catch each and every
event that they should act on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;your-own-operator&quot;&gt;Your own operator&lt;/h2&gt;

&lt;p&gt;So, why would you want to develop your own operator? Well, maybe your organisation offers a particular
product, let’s say some kind of communications service. Once your organisation chooses to use Kubernetes to deploy this
application, you can define your own resource type that describes the deployment of this application. You can define
security parameters, the way it is exposed to the outside world, the data storage it uses to persist information, the
amount of resources like memory and CPU it is allowed to take up, etcetera.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src=&quot;/images/generated-curly-braces-all-over-the-place.png&quot; alt=&quot;Curly braces all over the place. An AI-generated impression.&quot; /&gt;
  &lt;figcaption&gt;&quot;Curly braces all over the place&quot;. An AI-generated impression. Source:
    &lt;a href=&quot;https://vision-explorer.allenai.org/text_to_image_generation&quot;&gt;DeepAI.org Text To Image API&lt;/a&gt;
  &lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;If your application is relatively simple, you may use &lt;a href=&quot;https://helm.sh/&quot;&gt;Helm&lt;/a&gt; so you don’t need a custom resource to
describe your application to Kubernetes resources. If you can, you probably should. But if your self-defined application
definition requires more complicated logic, Helm is not such a good idea. Trying to put lots of “business logic” inside
Helm templates results in very hard to maintain Helm charts with curly braces all over the place. Helm is without a
doubt a very useful tool, and it will stay being a very useful tool even when you decide to write your own operator: you
can use Helm to deploy the operator! We won’t go into Helm in this tutorial, there are plenty of those around. Instead,
we’re going to take a look at when you want to implement your own operator.&lt;/p&gt;

&lt;h2 id=&quot;operator-development-and-operator-frameworks&quot;&gt;Operator development and operator frameworks&lt;/h2&gt;

&lt;p&gt;The current state of operator development is complex to say the least. The framework we used was the exact opposite of 
&lt;em&gt;everything&lt;/em&gt; you would want from an operator framework. The framework was complex, clunky, buggy, enormous,
the operator images we made were huge, the operator itself slow and heavy on resources, and hard to maintain. Which
framework we used doesn’t matter here, this is not a framework bashing post. All I will say is that &lt;em&gt;all&lt;/em&gt; of the options
listed under the operator frameworks to use for operator development are more complex than the one I’m about to show
you. Furthermore, all of them suffer from visibility problems that we’ll discuss in some detail below. So, let’s get to
it!&lt;/p&gt;

&lt;h2 id=&quot;argo-events-as-an-operator-framework&quot;&gt;Argo Events as an operator framework&lt;/h2&gt;

&lt;p&gt;For about a year now, I had been using &lt;a href=&quot;https://argoproj.github.io/argo-workflows/&quot;&gt;Argo Workflows&lt;/a&gt; to much
satisfaction. We used it to ingest data from external and internal sources, mostly targeting PostGIS databases to pipe
the data to. It has been working very nicely, and we created an operator and accompanying “custom resource definition”
that allowed us to describe a dataset ingestion operation, which worked very well for us.&lt;/p&gt;

&lt;p&gt;Then, we started looking into &lt;a href=&quot;https://argoproj.github.io/argo-events/&quot;&gt;Argo Events&lt;/a&gt;. It’s a great system for letting
other infrastructure components know that some state changes occurred, such as completing the ingestion of a data set.
Argo Events has a fantastic set of sources that it can listen to, and if that isn’t enough you can even define your own.
Argo Events splits event handling into nicely and cleanly separated parts with each its own responsibilities:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;an &lt;a href=&quot;https://argoproj.github.io/argo-events/concepts/eventbus/&quot;&gt;EventBus&lt;/a&gt;, which will keep event messages until
handled,&lt;/li&gt;
  &lt;li&gt;an &lt;a href=&quot;https://argoproj.github.io/argo-events/concepts/event_source/&quot;&gt;EventSource&lt;/a&gt;, which will put a message on an event
bus for a particular state change,&lt;/li&gt;
  &lt;li&gt;a &lt;a href=&quot;https://argoproj.github.io/argo-events/concepts/sensor/&quot;&gt;Sensor&lt;/a&gt;, which will listen on the event bus for particular
messages and translate these into actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This system alone is a very nice message bus that you may compare with (but is quite distinct from) products
like &lt;a href=&quot;https://www.rabbitmq.com/&quot;&gt;RabbitMQ&lt;/a&gt; (with which I had favourable experience)
and &lt;a href=&quot;https://kafka.apache.org/&quot;&gt;Kafka&lt;/a&gt; (with which I have no experience). The big difference is that Argo Events is,
like all Argo ecosystem products, built to work with Kubernetes. This means that it is probably more “cloud native” than
other event handling systems.&lt;/p&gt;

&lt;p&gt;One thing that struck me is that &lt;strong&gt;EventSources can put a message on the bus on the creation, change or deletion
of &lt;em&gt;any&lt;/em&gt; resource kind in the cluster&lt;/strong&gt;. Sounds familiar? It’s exactly the same role as the main responsibility of
Operators: to listen to particular custom resources and act on them. So, what if you could use Argo Events to define
your Operator, using just that or maybe a few scripts to handle the “business logic” part of the custom resource
handling?&lt;/p&gt;

&lt;p&gt;It took me a little while to work this out - like any framework on Kubernetes or even Kubernetes itself, it has its own
learning curve. But once I saw on how to combine Argo Events and Argo Workflows, it started to dawn on me: this is
better than &lt;em&gt;any&lt;/em&gt; current special-purpose operator framework and at the same time, it is useful for &lt;em&gt;a lot more&lt;/em&gt; as
well!&lt;/p&gt;

&lt;h2 id=&quot;lets-dig-in&quot;&gt;Let’s dig in&lt;/h2&gt;

&lt;p&gt;So to see this experiment in action, we are going
to &lt;a href=&quot;https://sdk.operatorframework.io/docs/building-operators/ansible/tutorial/&quot;&gt;replicate a tutorial from the Operator SDK&lt;/a&gt;
and create an operator that handles &lt;a href=&quot;https://www.memcached.org/&quot;&gt;Memcached&lt;/a&gt; deployments. Since I have not used Memcached
for anything myself, I can offer little information on its purpose other than that you can use it to take load of your
web services by caching http requests, for example. The idea is that, if your http gateway has handled a particular http
request, Memcached can keep a copy of the response and serve it much faster because the web service or database the
response originated from does not have to go through the operation of assembling the response. This does leave you with
the classic hard question on when to invalidate the cache - one of the
purportedly &lt;a href=&quot;https://www.martinfowler.com/bliki/TwoHardThings.html&quot;&gt;“two hard things in Computer Science”&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;1-install-argo-events&quot;&gt;1. Install Argo Events&lt;/h3&gt;

&lt;p&gt;The simple solution:
follow &lt;a href=&quot;https://argoproj.github.io/argo-events/installation/#cluster-wide-installation&quot;&gt;these steps&lt;/a&gt;. To keep things
very simple, we’ll go with the cluster-wide installation for now.&lt;/p&gt;

&lt;p&gt;NOTE: make sure you &lt;em&gt;only&lt;/em&gt; follow the “Cluster-wide Installation” steps, there are three of them. Skip the “Namespace 
Installation”.&lt;/p&gt;

&lt;h3 id=&quot;2-install-argo-workflows&quot;&gt;2. Install Argo Workflows&lt;/h3&gt;

&lt;p&gt;Why do we need &lt;a href=&quot;https://argoproj.github.io/argo-workflows/&quot;&gt;Argo Workflows&lt;/a&gt;? Because the operator “business logic” is
going to be handled in a Workflow. Think of Workflows as much, much better versions
of &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/job/&quot;&gt;Jobs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let’s start with installing Argo server and Workflows cluster-wide:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl create namespace argo
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo apply &lt;span class=&quot;nt&quot;&gt;-k&lt;/span&gt; github.com/argoproj/argo-workflows/manifests/cluster-install?ref&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;By default, Argo uses the Docker runtime executor. If you’re on Azure, or you use Podman or some other runtime execution&lt;br /&gt;
engine, be sure to patch the Argo Workflow executor:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Use &quot;pns&quot; executor instead of Docker:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# See https://github.com/argoproj/argo-workflows/issues/826#issuecomment-872426083&lt;/span&gt;
kubectl patch &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo configmap workflow-controller-configmap &lt;span class=&quot;nt&quot;&gt;--patch&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;data&quot;:{&quot;containerRuntimeExecutor&quot;:&quot;pns&quot;}}&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If your workflow pods fail to start or Argo gives you errors reaching the pods, then switching to a different runtime
executor will probably help. Also, for our tutorial we want to disable having to log in to access the Argo UI:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl patch &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo deployment argo-server &lt;span class=&quot;nt&quot;&gt;--patch&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;spec&quot;:{&quot;template&quot;:{&quot;spec&quot;:{&quot;containers&quot;:[{&quot;name&quot;:&quot;argo-server&quot;,&quot;args&quot;:[&quot;server&quot;,&quot;--auth-mode=server&quot;]}]}}}}&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once you have this set up, you can tunnel to the Argo UI using:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo port-forward svc/argo-server 2746:2746
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can access the UI on &lt;a href=&quot;https://localhost:2746&quot;&gt;https://localhost:2746&lt;/a&gt;. The default for the server is to run over
https, so you will get a self-signed certificate warning. As the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;port-forward&lt;/code&gt; option claims the terminal (the tunnel
is active until you disconnect using ctrl-c), you will have to open a new terminal for the commands following down
below.&lt;/p&gt;

&lt;p&gt;The most important part here is that the installation comes
with &lt;a href=&quot;https://argoproj.github.io/argo-workflows/argo-server/&quot;&gt;Argo Server&lt;/a&gt;, the UI that allows you to inspect both
EventSources, Sensors and, if you generate them from the Sensor triggers, Workflows. Personally, I’d skip the step to
install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argo&lt;/code&gt; command line tool, I found it to be of little use. It is, however, very useful in CI test setups,
such as
the &lt;a href=&quot;https://github.com/reinvantveer/reinvantveer.github.io/blob/master/.github/workflows/argo-events-memcached-operator-test.yaml&quot;&gt;one I made for this tutorial&lt;/a&gt;
.&lt;/p&gt;

&lt;h3 id=&quot;3-install-the-memcached-custom-resource-definition&quot;&gt;3. Install the Memcached custom resource definition&lt;/h3&gt;

&lt;p&gt;Installing the Argo components can take like a minute to start up. Once it’s up and running, we can start deploying our
custom resources. This is not intended to be a full Operator tutorial, so I’ll try to keep things as simple as possible.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apiextensions.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CustomResourceDefinition&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# This must be a combination of the spec.names.plural + spec.group&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcacheds.cache.example.com&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# This is the api &quot;namespace&quot; you define for yourself.&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Usually some subdomain of your organisation site url.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cache.example.com&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# This defines the names under which you can list created resources of this kind,&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# using kubectl for example&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Memcached&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;plural&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcacheds&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Either Namespaced or Cluster-scoped.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;scope&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Namespaced&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# `versions` is mandatory:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# there should be at least one version that is served and stored.&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;versions&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# You usually start out with some kind of alpha version.&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Be sure to create a roadmap on how to get to a stable &quot;v1&quot;.&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1alpha1&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;served&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# See also: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#create-a-customresourcedefinition&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;schema&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;openAPIV3Schema&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;object&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Like the `spec` key this key resides under, any custom resource&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# created as kind `Memcached` will need a `spec` key.&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;object&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;properties&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# So the only spec key will be a `size` parameter as integer&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# All tutorial business, of course&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;integer&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;The number of replicas for the Memcached deployment&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can simply install this with&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/crd.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;4-create-the-service-account-and-access-rights&quot;&gt;4. Create the service account and access rights&lt;/h3&gt;
&lt;p&gt;By default, the Kubernetes API doesn’t allow access to our newly devised custom resource. By basically anything - we
need to tell the Kubernetes API that it’s OK, provided a special service account is used to access it. Like the custom 
resource definition, this is basic Operator engineering 101. This will be our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memcached-sa&lt;/code&gt; service account, that comes
with the associated rights:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# This is just a tutorial setup - adapt&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# to your access control requirements&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-role&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterRole&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;verbs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;get&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;list&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;create&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;watch&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;patch&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;update&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;post&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;put&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;delete&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcacheds&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;workflows&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;workflowtemplates&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deployments&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pods&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pods/log&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pods/exec&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;apiGroups&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cache.example.com&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argoproj.io&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Core API group: for pods&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterRoleBinding&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-rb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;roleRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-role&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;apiGroup&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;rbac.authorization.k8s.io&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterRole&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;subjects&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ServiceAccount&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# In case you installed Argo Events in a different namespace:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# adapt namespace here&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argo-events&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ServiceAccount&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argo-events&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Install with:&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo-events &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/rbac.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;4-install-the-eventsource&quot;&gt;4. Install the EventSource&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventSource&lt;/code&gt; specifies what to listen to - in this case our custom “Memcached” resources. We specify that we listen
to all life cycle events. In event-speak, these are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;EventSource&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-source&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Assign a service account with `get`, `list` and `watch` permissions&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# on the resource being listened to.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;memcached&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;group&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cache.example.com&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1alpha1&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcacheds&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;eventTypes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Types: https://github.com/argoproj/argo-events/blob/00e2ae801addcd362a22613a745ae424932efa40/pkg/apis/eventsource/v1alpha1/types.go#L280&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Listen to all lifecycle events of the specified custom resource&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ADD&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;UPDATE&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DELETE&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;afterStart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can install this with&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo-events apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/event-source.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And we can see it appearing in the UI:
&lt;img src=&quot;/images/argo-operator/event-source-in-argo-ui.png&quot; alt=&quot;Event Source in Argo UI&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;5-install-the-sensor&quot;&gt;5. Install the Sensor&lt;/h3&gt;

&lt;p&gt;The sensor specifies what to do with the event messages generated by our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EventSource&lt;/code&gt;. In our case, we offload the work
to an Argo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Workflow&lt;/code&gt; that calls a WorkflowTemplate (see below) with a few parameters from the event message:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Sensor&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sensor&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;source-dependency&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# References the metadata.name in the event source&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;eventSourceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-source&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# References the key directly under &quot;resource:&quot; in the event source&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;eventName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;triggers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argo-workflow-trigger&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;argoWorkflow&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;submit&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argoproj.io/v1alpha1&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Workflow&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;generateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-memcached-workflow-&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-memcached-template&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;# These values get overridden, see `parameters` below&lt;/span&gt;
                    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-manifest&lt;/span&gt;
                      &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
                    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operation&lt;/span&gt;
                      &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;
                    &lt;span class=&quot;c1&quot;&gt;# These values are required for the Sensor to deploy&lt;/span&gt;
                    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;script&lt;/span&gt;
                      &lt;span class=&quot;na&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;na&quot;&gt;configMapKeyRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                          &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-cm&lt;/span&gt;
                          &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main.py&lt;/span&gt;
                    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deployment-manifest&lt;/span&gt;
                      &lt;span class=&quot;na&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;na&quot;&gt;configMapKeyRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                          &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-cm&lt;/span&gt;
                          &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deployment-manifest&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;templates&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-memcached-template&lt;/span&gt;
                    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                      &lt;span class=&quot;c1&quot;&gt;# `steps` may look a little funky, as lists of lists:&lt;/span&gt;
                      &lt;span class=&quot;c1&quot;&gt;# the outer lists are run sequentially, the inner ones&lt;/span&gt;
                      &lt;span class=&quot;c1&quot;&gt;# in parallel&lt;/span&gt;
                      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;call-template&lt;/span&gt;
                          &lt;span class=&quot;c1&quot;&gt;# We call the template reference listed under `name`,&lt;/span&gt;
                          &lt;span class=&quot;c1&quot;&gt;# from the template onwards listed under `template`&lt;/span&gt;
                          &lt;span class=&quot;na&quot;&gt;templateRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-deploy-workflow-template&lt;/span&gt;
                            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;

          &lt;span class=&quot;c1&quot;&gt;# These are the parameters of what data gets selected from the event bus&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# message into the resource generated by the sensor&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;dependencyName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;source-dependency&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# The message body is the Memcached custom resource&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;dataKey&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;body&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;spec.arguments.parameters.0.value&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;dependencyName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;source-dependency&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# This is the `operation` parameter to the workflow,&lt;/span&gt;
                &lt;span class=&quot;c1&quot;&gt;# one of ADD, UPDATE, or DELETE, covering the entire life cycle&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;dataKey&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;type&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;dest&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;spec.arguments.parameters.1.value&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can install this with&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo-events &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/sensor.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And it appears in the UI:
&lt;img src=&quot;/images/argo-operator/sensor-in-argo-ui.png&quot; alt=&quot;Sensor in Argo UI&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;6-install-the-configmap-containing-the-operator-implementation-details&quot;&gt;6. Install the configmap containing the operator implementation details&lt;/h3&gt;

&lt;p&gt;Instead of putting all implementation details for the operator into the Workflow or Workflow Template (see next step),
we use a common practice in Kubernetes: a &lt;a href=&quot;https://kubernetes.io/docs/concepts/configuration/configmap/&quot;&gt;ConfigMap&lt;/a&gt;.
ConfigMaps hold configuration, in our case the script we use to configure the Memcached deployment and the deployment
template for our Memcached deployment.&lt;/p&gt;

&lt;p&gt;The main reason why this is a good idea is that operators tend to grow in complexity and with it the lines of code
required to configure the target resources to apply to the cluster. Putting all this into the WorkflowTemplate will
result in larger and larger WorkflowTemplates that become harder to maintain. With ConfigMaps we can “modularize” our
operator logic into separate files that are stored in separate ConfigMap keys or even into separate ConfigMaps if we 
like:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ConfigMap&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-cm&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Note that this label is required for the informer to detect this ConfigMap.&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# See also: https://github.com/argoproj/argo-workflows/blob/master/examples/configmaps/simple-parameters-configmap.yaml&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# https://github.com/argoproj/argo-workflows/blob/master/examples/arguments-parameters-from-configmap.yaml&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;workflows.argoproj.io/configmap-type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Parameter&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;main.py&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;import argparse&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;import json&lt;/span&gt;
    
    &lt;span class=&quot;s&quot;&gt;parser = argparse.ArgumentParser(description=&apos;Memcached to deployment&apos;)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;parser.add_argument(&apos;--memcached-manifest&apos;, required=True)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;parser.add_argument(&apos;--deployment-manifest&apos;, required=True)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;args = parser.parse_args()&lt;/span&gt;

    &lt;span class=&quot;s&quot;&gt;memcached = json.loads(args.memcached_manifest)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;deployment = json.loads(args.deployment_manifest)&lt;/span&gt;
    
    &lt;span class=&quot;s&quot;&gt;deployment[&apos;metadata&apos;][&apos;namespace&apos;] = memcached[&apos;metadata&apos;][&apos;namespace&apos;]&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;deployment[&apos;spec&apos;][&apos;replicas&apos;] = int(memcached[&apos;spec&apos;][&apos;size&apos;])&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;result = json.dumps(deployment, indent=2)&lt;/span&gt;
    
    &lt;span class=&quot;s&quot;&gt;# Printed output goes into outputs.parameters.result&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;with open(&apos;/output-manifest.json&apos;, &apos;wt&apos;) as f:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;f.write(result)&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Storing the manifest in JSON allows us to use the Python standard library&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;deployment-manifest&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;&quot;apiVersion&quot;: &quot;apps/v1&quot;,&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;&quot;kind&quot;: &quot;Deployment&quot;,&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;&quot;metadata&quot;: {&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;name&quot;: &quot;memcached-deployment&quot;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;labels&quot;: {&quot;app&quot;: &quot;memcached&quot;}&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;},&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;&quot;spec&quot;: {&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;replicas&quot;: 1,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;selector&quot;: {&quot;matchLabels&quot;: {&quot;app&quot;: &quot;memcached&quot;}},&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;template&quot;: {&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;&quot;metadata&quot;: {&quot;labels&quot;: {&quot;app&quot;: &quot;memcached&quot;}},&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;&quot;spec&quot;: {&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;containers&quot;: [{&lt;/span&gt;
              &lt;span class=&quot;s&quot;&gt;&quot;name&quot;: &quot;memcached&quot;,&lt;/span&gt;
              &lt;span class=&quot;s&quot;&gt;&quot;command&quot;: [&quot;memcached&quot;, &quot;-m=64&quot;, &quot;-o&quot;, &quot;modern&quot;, &quot;-v&quot;],&lt;/span&gt;
              &lt;span class=&quot;s&quot;&gt;&quot;image&quot;: &quot;docker.io/memcached:1.4.36-alpine&quot;,&lt;/span&gt;
              &lt;span class=&quot;s&quot;&gt;&quot;ports&quot;: [{&quot;containerPort&quot;: 11211}]&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;}]&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you can see, this ConfigMap also holds the “template” we use for our Memcached deployment. Instead of YAML, we 
express this manifest in JSON because this is much easier to load. Many programming languages come with a JSON parser in
the standard library (think Python, JavaScript, Ruby, Go, PHP, etc), but I know of not a single language with a YAML
parser in the standard lib. Since &lt;em&gt;any&lt;/em&gt; JSON is valid YAML, we can pass the result as JSON and be a valid output 
manifest.&lt;/p&gt;

&lt;p&gt;Install with&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo-events &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/configmap.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;7-install-the-workflow-template-to-deploy-memcached-resources&quot;&gt;7. Install the workflow template to deploy Memcached resources&lt;/h3&gt;

&lt;p&gt;Finally, we’re deploying the Argo &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WorkflowTemplate&lt;/code&gt; that takes care of the deployment for us - the “heavy lifting” that
the Sensor delegated to the Workflow:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#  The workflow template that does the &quot;heavy lifting&quot;&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;WorkflowTemplate&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-deploy-workflow-template&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# See the `main` template below: this gets executed&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Use the service account with sufficient rights to deploy&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-sa&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ttlStrategy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;secondsAfterCompletion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;300&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Garbage collection settings for clearing up old pods&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;podGC&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;strategy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;OnPodSuccess&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# Global Workflow parameters, passed/overridden from the Sensor&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Injected by Sensor workflow&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-manifest&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Sets a compact default which we can test-run in Argo UI&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;{ &quot;apiVersion&quot;: &quot;cache.example.com/v1alpha1&quot;, &quot;kind&quot;: &quot;Memcached&quot;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;metadata&quot;: { &quot;name&quot;: &quot;memcached&quot;, &quot;namespace&quot;: &quot;argo-events&quot; },&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;spec&quot;: { &quot;size&quot;: &quot;3&quot; } }&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Injected by Sensor workflow&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operation&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ADD&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;script&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;configMapKeyRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-cm&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main.py&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deployment-manifest&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;configMapKeyRef&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached-cm&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deployment-manifest&lt;/span&gt;

  &lt;span class=&quot;na&quot;&gt;templates&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# A simple logging container template - we&apos;ll use this to debug&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;content&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;simple-logger&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu:latest&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;echo&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{inputs.parameters.content}}&quot;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# The actual &quot;business logic&quot; for our operator.&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# As tutorials go, it&apos;s a bit contrived - there&apos;s very little&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# action going on&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operator-logic&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python-operator-logic&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python:3.10-slim&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python3&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-c&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{workflow.parameters.script}}&quot;&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--deployment-manifest={{workflow.parameters.deployment-manifest}}&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--memcached-manifest={{workflow.parameters.memcached-manifest}}&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;outputs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;result&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;valueFrom&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/output-manifest.json&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operation-mapping&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python:3.10-slim&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python3&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# The &quot;operations&quot; from our event source differ from the ones used in the&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# deployment syntax - we need to map from one to the other&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;mapping = {&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;ADD&quot;: &quot;apply&quot;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;UPDATE&quot;: &quot;apply&quot;,&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;DELETE&quot;: &quot;delete&quot;,&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;print(mapping[&quot;{{workflow.parameters.operation}}&quot;])&lt;/span&gt;

    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;action&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;manifest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{inputs.parameters.action}}&quot;&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;manifest&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{inputs.parameters.manifest}}&quot;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# This is the actual entrypoint for our workflow&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# It takes care of the execution flow&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log-memcached-manifest&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Uses the &quot;log&quot; template&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;content&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{workflow.parameters.memcached-manifest}}&quot;&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log-operation&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Again: uses the &quot;log&quot; template below, so we only need to define once&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;content&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{workflow.parameters.operation}}&quot;&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;parameterize-replicas&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operator-logic&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log-result&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;content&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{steps.parameterize-replicas.outputs.parameters.result}}&quot;&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;map-operation-to-action&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# Calls the actual operator &quot;business logic&quot; part&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;operation-mapping&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log-action&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;log&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;content&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{steps.map-operation-to-action.outputs.result}}&quot;&lt;/span&gt;

        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-memcached&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;action&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{steps.map-operation-to-action.outputs.result}}&quot;&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;manifest&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;{{steps.parameterize-replicas.outputs.parameters.result}}&quot;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Done! &lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Install with&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; argo-events &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/workflow-template.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Voilà, the workflow template is present in the Argo UI:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/argo-operator/workflow-template-in-argo-ui.png&quot; alt=&quot;Memcached deployment workflow template in Argo UI&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is a fully functional workflow template. As most workflow templates, we can issue workflows from it, using the Argo
UI using the “submit” button. You can test your operator logic in this way. Note that this does not test the correctness
of the Sensor, though!&lt;/p&gt;

&lt;h3 id=&quot;8-lets-go&quot;&gt;8. Let’s go!&lt;/h3&gt;

&lt;p&gt;Now we can deploy our Memcached instance:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cache.example.com/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Memcached&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;memcached&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# Let&apos;s try it out!&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;which we should be able to, in good operator-fashion, apply in any namespace we want in our cluster. Let’s say that we
want our Memcached deployment to go into the “services” namespace:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create fresh namespace&lt;/span&gt;
kubectl create namespace services
&lt;span class=&quot;c&quot;&gt;# This is where we want our Memcached deployment to live:&lt;/span&gt;
kubectl apply &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; services &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/memcached.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And the workflow completes, deploying our application:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/argo-operator/completed-memcached-deployment-workflow.png&quot; alt=&quot;Workflow deploying Memcached instance&quot; /&gt;&lt;/p&gt;

&lt;p&gt;How can we tell it’s running?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/argo-operator/running-deployment-viewed-with-lens.png&quot; alt=&quot;Memcached running&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And we can watch as our operator cleans it up again:&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; services &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/reinvantveer/reinvantveer.github.io/master/_includes/memcached/memcached.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 29 Jan 2022 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2022/01/29/easier-operator.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2022/01/29/easier-operator.html</guid>
        
        
      </item>
    
      <item>
        <title>A Quality Definition of Done for Python projects</title>
        <description>&lt;h2 id=&quot;setting-an-internal-standard-for-code-quality-for-your-python-software&quot;&gt;Setting an internal standard for code quality for your Python software&lt;/h2&gt;

&lt;p&gt;I love Python because it’s concise, readable and its speed is sufficient for most of the work I do. What I particularly
like about the language is the fact that it really encourages collaboration between team members. In Python, it’s just a
little harder to make a mess of things than in some other programming languages. However, this does not come without 
issues.&lt;/p&gt;

&lt;p&gt;This article is about how to manage code quality around team-maintainable Python software projects. It’s based on quite
a bit of hard-won personal experience. My recommendations certainly are not meant as a pedantic dictate. However, I
intend to clear up any questions on why each recommendation for a quality DoD is of particular value.&lt;/p&gt;

&lt;h3 id=&quot;development-and-definitions-of-done-why-code-quality-matters&quot;&gt;Development and Definitions of Done: why code quality matters&lt;/h3&gt;
&lt;p&gt;It’s not uncommon for teams to have an unclear picture of what constitutes the definition of done for doing work, maybe
even more so for Python projects. Often team members are just starting out and quite possibly are even new to Python 
itself. Also, more often than not, Product Owners or other team leads often fail to see the importance of code quality
when they realize that more time is required on a product that already “just works”. If the customer is satisfied, why
do anything anymore?&lt;/p&gt;

&lt;h3 id=&quot;does-this-sound-like-a-familiar-scenario-already&quot;&gt;Does this sound like a familiar scenario already?&lt;/h3&gt;
&lt;p&gt;Many code repositories start to rot when they are hard to maintain. This isn’t difficult to see: someone wrote
a bunch of Python code, but no-one really grasps what has been written and why. Often a complex intertwining of Python, 
SQL, some RegEx sprinkled on top, maybe some JavaScript, Bash or R thrown in, and suddenly:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The “someone” who wrote all the code moves along to another position in the company, or leaves for another.&lt;/li&gt;
  &lt;li&gt;The product that once “just worked” suddenly “just doesn’t” after a few months and nobody seems to know how to fix it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So what just happened? Probably the team didn’t set a clear definition of done for this project to be maintained &lt;em&gt;by a
team&lt;/em&gt;. This team-maintainability is a big thing. Only if the team understands the code base, then the code base can be
maintained by it. This sounds gratuitous, but often shared knowledge is really hard for teams to maintain. Probably
someone (“the SQL guy”) got type-cast into a role that nobody really understood. Probably the team ran out of time at
some point at which the code wasn’t reviewed.&lt;/p&gt;

&lt;p&gt;This is why having a solid agreement within the team of what constitutes good quality code is important. Even if human
or financial resources are constrained at some point, at least team members can agree on that &lt;em&gt;at some other point&lt;/em&gt; in
time, this technical debt has to be fixed.&lt;/p&gt;

&lt;p&gt;So what follows here is what I consider a good set of quality standards to agree upon, and why they matter.&lt;/p&gt;

&lt;h2 id=&quot;a-set-of-dod-quality-standards&quot;&gt;A set of DoD quality standards&lt;/h2&gt;

&lt;h3 id=&quot;document-your-project-and-agree-on-what-it-means-to-document&quot;&gt;Document your project and agree on what it means to document&lt;/h3&gt;
&lt;p&gt;This may be harder than it sounds, but you need to document your code in order for future maintainers of your code. This
isn’t hard to grasp or accept for anyone I met in IT. I recently found out that there’s some best practices for this,
but you probably need to write &lt;em&gt;different kinds&lt;/em&gt; of documentation. Write inline comments and docstrings. Document
&lt;a href=&quot;https://www.youtube.com/watch?v=bQSR1UpUdFQ&quot;&gt;how your documentation is organized&lt;/a&gt;, write tutorials and topic guides. If
your project is itself a library, then compile reference guides from your docstrings using
&lt;a href=&quot;https://www.sphinx-doc.org/en/master/&quot;&gt;Sphinx&lt;/a&gt;, and write how-to guides. Agree with your teammates on what should and
what shouldn’t be included in the documentation, agree on some kind of documentation template that will make things
easier for everyone.&lt;/p&gt;

&lt;h3 id=&quot;review-each-others-code-in-a-source-code-management-system&quot;&gt;Review each other’s code in a source code management system&lt;/h3&gt;

&lt;p&gt;There really is no way around this one. If you want your team members to be in on what you write and why, have your code
reviewed by someone. Probably by someone with less experience. Why? Make sure they understand what you wrote and why,
even if it means admitting for that person that they don’t understand everything and that that is quite OK. Share your
knowledge and allow others to grow. If you are uncertain on reviewing some issues, just ask another team member to pitch
in. It almost goes without saying that you should use some kind of source control management system
like Git, Fossil or perhaps even Mercurial in order to keep track of changes but there is generally no disagreement on
this. Almost everyone uses Git.&lt;/p&gt;

&lt;h3 id=&quot;write-automated-tests-for-your-code&quot;&gt;Write automated tests for your code&lt;/h3&gt;

&lt;p&gt;If you write code for your team, help them out maintaining your code by writing tests. Tests are there to make sure that
the product meets the requirements. There’s a pretty big chance that the &lt;em&gt;only&lt;/em&gt; place where the formal requirements for
your product are documented, is in your tests! Writing tests means that, once the code needs to be changed, these
requirements are still met. Have the requirements changed? Change the tests! There’s simply no good excuse not to write
tests and I’m pretty sure I’ve heard all of them:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“There’s not enough time”, which is probably favourite. Is there time a couple of months down the road to spend days
on figuring out what the code does and why? Is there time a couple of years down the road to have to re-write 
everything because the requirements are unknown? If there is no time to write tests, don’t do the project.&lt;/li&gt;
  &lt;li&gt;“We don’t write tests here”. You should have these people read the 
&lt;a href=&quot;https://en.wikipedia.org/wiki/Therac-25&quot;&gt;horrifying case of the THERAC-25&lt;/a&gt; and ask them again if they still think it
unnecessary to write tests. Writing tests is an &lt;em&gt;ethical obligation&lt;/em&gt; for software developers. It’s simply 
irresponsible not to do so.&lt;/li&gt;
  &lt;li&gt;“I don’t know how to write tests”. Yes, this is a tricky one. Writing tests can be difficult, in fact often it’s more
difficult than writing the production code itself. Fortunately, there’s plenty of useful resources out there, just 
&lt;a href=&quot;https://duckduckgo.com/?q=writing+tests+in+python&quot;&gt;do a web search&lt;/a&gt; for them. If you need advice: I’d start with 
&lt;a href=&quot;https://pytest.org/&quot;&gt;pytest&lt;/a&gt;, it’s probably the most sensible framework to use these days. But most importantly:
collaborate with teammates on learning to write good tests. Practice, and you’ll get better in it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;use-a-style-checker&quot;&gt;Use a style checker&lt;/h3&gt;

&lt;p&gt;Style checkers can catch an incredibly large set of “code smells” or just stuff you forgot to put right. Unused imports,
unused variables, unreachable code, indentation errors, you name it. All these things can confuse your fellow team
members considerably (“why is this here???”), reduce the cognitive load and probably make your code run smoother as
well. Often, there’s little time involved in type-checking your code. If you need advice: use
&lt;a href=&quot;https://flake8.pycqa.org/en/latest/&quot;&gt;flake8&lt;/a&gt; with this configuration in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.cfg&lt;/code&gt; (adapted from 
&lt;a href=&quot;https://flake8.pycqa.org/en/latest/user/configuration.html&quot;&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;div class=&quot;language-ini highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[flake8]&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# The .git folder only holds version control information
# Cached directories only hold compiled Python files that can&apos;t be checked
# Build and dist directories contain derived artifacts
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;exclude&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.git, __pycache__, build, dist&lt;/span&gt;
&lt;span class=&quot;py&quot;&gt;max-complexity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;10&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use modern IDE line length settings as default. We&apos;re not in teletype-age anymore
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;max-line-length&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;121&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;type-check-your-python-code&quot;&gt;Type-check your Python code&lt;/h3&gt;
&lt;p&gt;In case you’re not familiar with type hints in Python: start &lt;a href=&quot;https://realpython.com/lessons/type-hinting/&quot;&gt;here&lt;/a&gt;. 
Dynamic typing is a big source of both runtime failures and confusion in Python code. Use
&lt;a href=&quot;https://mypy.readthedocs.io&quot;&gt;mypy&lt;/a&gt; or another type checker to make sure that you got your types right. Writing typed
python helps for no less than three quality aspects:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;To make sure that you pass the right arguments to functions, and handle its return types appropriately&lt;/li&gt;
  &lt;li&gt;It clarifies to your teammates what a function or method expects. Is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt; a list? A dictionary? An object? Specify
the type of it!&lt;/li&gt;
  &lt;li&gt;It will probably allow your code to run faster in the not-too-distant future. If the compiler has knowledge of what
type your variable is, it can optimize for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use mypy and add to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setup.cfg&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-ini highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;[mypy]&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Ignore dependencies that do not have typings - otherwise this renders many libraries useless
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;ignore_missing_imports&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Don&apos;t allow the `Any` type
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;disallow_any_explicit&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Skip checking site packages for typing
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;no_site_packages&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Require type hints on all functions and methods
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;disallow_untyped_defs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Disallows defining functions with incomplete type annotations.
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;disallow_incomplete_defs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Type-checks the interior of functions without type annotations.
&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;check_untyped_defs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;use-a-dependency-manager-for-your-installed-libraries&quot;&gt;Use a dependency manager for your installed libraries&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pip&lt;/code&gt; as a package installer has worked great for a while, but there are a few ingredients missing in pip as a package
manager that allows you to create &lt;em&gt;reproducible builds&lt;/em&gt;. For example: the default way of specifying which packages your
project requires used to be in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requirements.txt&lt;/code&gt; file. However, there were a few shortcomings in this strategy. It
was kinda hard to specify separate sources for your libraries (say, pypi.org or your private company-hosted python
libraries), you had to keep track of what you installed along the way, and splitting dev-dependencies such as type and
style checkers from production dependencies was a bit of a hassle. And then, you had to manage your virtual
environments. Nowadays, &lt;a href=&quot;https://pipenv.pypa.io/en/latest/&quot;&gt;pipenv&lt;/a&gt; can take care of all this stuff for you. I’d
recommend pipenv for this, but you could also go for &lt;a href=&quot;https://python-poetry.org/&quot;&gt;poetry&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;run-your-tests-and-checks-in-a-continuous-integration-environment&quot;&gt;Run your tests and checks in a continuous integration environment&lt;/h3&gt;
&lt;p&gt;When you have all the above parts in place, use some kind of continuous integration tool in order to make sure that all
is OK once you check your code into a repository. If you don’t have anything in place, start with GitHub Workflows, it’s
fantastic!&lt;/p&gt;

&lt;p&gt;The advantages of having CI are many, but regarding team collaboration: it’s the transparency that it offers. Your code
may run or check out on &lt;em&gt;your&lt;/em&gt; machine, but it may not on your teammate’s. A “neutral ground” of CI allows you to view
the result your tests and checks on a clean working environment. If it exits in error, you have a hyperlink to point at
and ask your colleague if they can help resolving the issue. If your teammate is having trouble getting his checks to
pass but the CI pipeline doesn’t, maybe it’s time to clean some stuff out on their end. In the end, it should be your CI
pipeline that should give the final formal check on whether a revision of the product can get the go-ahead.&lt;/p&gt;
</description>
        <pubDate>Tue, 20 Jul 2021 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2021/07/20/python-quality-dod.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2021/07/20/python-quality-dod.html</guid>
        
        
      </item>
    
      <item>
        <title>A Motorola 68000 (m68k) retro computing project</title>
        <description>&lt;p&gt;Last week I finished my first actually working retro computing project: a 1980’s Motorola 68000 educational board 
&lt;a href=&quot;https://github.com/jefftranter/68000&quot;&gt;remake by Jeff Tranter&lt;/a&gt;. The original design is called a TS2, Jeff did a great 
job modernizing it, swapping hard to find chips with more easily obtainable ones and creating an &lt;a href=&quot;https://github.com/jefftranter/68000/tree/master/TS2/v2.1&quot;&gt;open PCB design for it
in KiCad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I can connect to it using PuTTY:
&lt;img src=&quot;/images/running-68000.jpg&quot; alt=&quot;Running Motorola 68000 educational board&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s hard to actually see, but in the top right of the picture the terminal says:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;TUTOR 1.3 &amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’m pretty proud that I have it up and running that far, it isn’t my first attempt at building retro computer hardware
and it can be both awesome and pretty difficult at times. Debugging hardware with my scope is a skill that comes with
its own learning curve.&lt;/p&gt;

&lt;p&gt;I had a couple of earlier attempts. One was on a hybrid &lt;a href=&quot;https://www.bigmessowires.com/68-katy/&quot;&gt;68008/CPLD board&lt;/a&gt; by
Steve “Big Mess O’Wires” Chamberlin. The design is pretty cool because it uses an easier to use Motorola 68008 which is 
code-compatible with the 68000 but has a smaller address and data bus, so less things to mess up. Also, it has enough 
RAM and ROM to run an early version 2 of the Linux kernel! But it proved too difficult for my humble soldering skills to
get the CPLD SMD flat package soldered on the board. So now I just stick to through-hole soldering :)&lt;/p&gt;

&lt;p&gt;This brought me to Jeff Tranter’s 68000 project. He created the PCB and component placement based on Alan Clements’ 
“Computer Systems Design”, on of the few books of which I own two editions (one for the included CD-ROM).
&lt;img src=&quot;/images/alan-clements-computer-systems-design.jpg&quot; alt=&quot;Alan Clements &amp;quot;Computer Systems Design&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The TS2 educational board design is described in detail by &lt;a href=&quot;https://github.com/jefftranter/68000/blob/master/TS2/v2.1/theoryofoperation.txt&quot;&gt;Jeff Tranter 
here&lt;/a&gt;, but it’s peculiar in several
aspects:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;It uses &lt;em&gt;no programmable chips&lt;/em&gt; anywhere except for the ROMs of course. That means that all the address decoding logic, 
power-on reset circuitry and data acknowledgement circuitry is implemented in TTL logic. The chip count is, 
expectedly, on the large size with 36 (!) chips on a pretty sizeable board. The upside is, of course, the cost: the 
TS2 board was supposedly a lot cheaper to build than &lt;a href=&quot;http://www.easy68k.com/paulrsm/mecb/mecb.htm&quot;&gt;Motorola’s own Educational Computer 
Board&lt;/a&gt; that went for a &lt;a href=&quot;http://www.s100computers.com/My%20System%20Pages/68000%20Board/The%20M68000%20Educational%20Computer%20Board.pdf.&quot;&gt;rather pricey $495
&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Despite the chip count, only 32kb of ROM and 32kb of RAM are available. This makes sense from an educational point of
view: there’s no requirement for large software packages to run on this kind of hardware.&lt;/li&gt;
  &lt;li&gt;It has a “single-stepping” (or more aptly a single-bus-cycle-stepping) circuit that can be operated using physical 
switches.&lt;/li&gt;
  &lt;li&gt;There’s data acknowledgement speed selection options using jumpers 1 thru 4 to allow for slower, cheaper RAM or ROM
chips. Jeff swapped ultraviolet-erasable 2764 ROM chips to electrically-erasable 28C64s, which are of course much 
easier to use, for example with ubiquitous and low-cost TL866II-type programmers.&lt;/li&gt;
  &lt;li&gt;The current board design has no keyboard input, no computer screen output and no board connectors other than two 
serial ports and the power connector. This means it’s pretty much impossible to attach board peripherals at the 
moment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All in all, it is a very cool build, I contributed back some minor updates to the board and the software and I hope to 
do some more cool stuff with it, write some programs and flash it to the board. I don’t know whether it will fit in ROM,
but porting a &lt;a href=&quot;http://download.minix3.org/previous-versions/bzipped/Amiga.tar.bz2&quot;&gt;version 2 of minix&lt;/a&gt; to the TS2 would
be awesome.&lt;/p&gt;

&lt;p&gt;Jeff’s design featured on &lt;a href=&quot;https://hackaday.com/2017/02/27/an-old-68000-sbc-is-new-again/&quot;&gt;Hackaday.io in February 
2017&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Mon, 10 May 2021 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2021/05/10/m68k-retro-computing-project.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2021/05/10/m68k-retro-computing-project.html</guid>
        
        
      </item>
    
      <item>
        <title>QGIS plugin development</title>
        <description>&lt;h1 id=&quot;tldr&quot;&gt;TL;DR:&lt;/h1&gt;

&lt;p&gt;This isn’t a beginners’ guide to QGIS plugin development, there
are &lt;a href=&quot;https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/plugins.html#writing-a-plugin&quot;&gt;other&lt;/a&gt;
excellent &lt;a href=&quot;https://medium.com/@abesingh1/writing-qgis-plugin-using-python-3-a-beginners-guide-ddf0be7e5357&quot;&gt;resources&lt;/a&gt;
out &lt;a href=&quot;https://snorfalorpagus.net/blog/2014/01/04/writing-unit-tests-for-qgis-python-plugins/&quot;&gt;there&lt;/a&gt; (although some of
them may be somewhat dated, as this post will some day be as well). This post discusses how to design integration tests
for python plugin scripts that target QGIS version something like 3.18. They were tested on 3.18. If you’re targeting a
newer version, things may not work as explained here.&lt;/p&gt;

&lt;p&gt;My take on the approach is &lt;a href=&quot;https://github.com/reinvantveer/namari&quot;&gt;all open here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My &lt;a href=&quot;https://github.com/reinvantveer/namari/blob/main/Dockerfile&quot;&gt;Dockerfile is here&lt;/a&gt;, and the &lt;a href=&quot;https://github.com/reinvantveer/namari/blob/main/test/integration_test.py&quot;&gt;integration test is here&lt;/a&gt;. The test is run using this &lt;a href=&quot;https://github.com/reinvantveer/namari/blob/main/.github/workflows/build.yml&quot;&gt;GitHub Actions spec&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Use &lt;a href=&quot;https://hub.docker.com/layers/qgis/qgis/latest/images/sha256-31926216a3bea81550a63accd5787592ddd602386ac5355a8e0a32e3b69c7385?context=explore&quot;&gt;this QGIS Docker image&lt;/a&gt;
to run your integration tests&lt;/li&gt;
  &lt;li&gt;Derive your own Dockerfile from this image, copy your plugin and tests into the image&lt;/li&gt;
  &lt;li&gt;Install the plugin to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/QGIS/build/output/python/plugins&lt;/code&gt; (possibly subject to future change)&lt;/li&gt;
  &lt;li&gt;Add the path &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/QGIS/build/output/python/plugins&lt;/code&gt; to the PYTHONPATH&lt;/li&gt;
  &lt;li&gt;Add an integration test script containing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;def run_all()&lt;/code&gt; function. This function is expected to be present.&lt;/li&gt;
  &lt;li&gt;Inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_all()&lt;/code&gt; function, instantiate your plugin using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;classFactory&lt;/code&gt; function. This is what QGIS would
normally call as well to instantiate your plugin and it should be part of the integration test to verify it’s working
correctly.&lt;/li&gt;
  &lt;li&gt;Put your (calls to) integration tests in this function.&lt;/li&gt;
  &lt;li&gt;If you use the standard lib &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unittest&lt;/code&gt; module and define a `class MyTestCase(unittest.TestCase), and execute it using
something like
    &lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
     &lt;span class=&quot;n&quot;&gt;suite&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unittest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TestLoader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loadTestsFromTestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MyTestCase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
     &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unittest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextTestRunner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;verbosity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;suite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wasSuccessful&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;If you want to write &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pytest&lt;/code&gt; tests, add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print(&apos;Ran OK&apos;)&lt;/code&gt; line in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_all&lt;/code&gt; function, otherwise the integration script will not recognize the test as successful.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COPY&lt;/code&gt; over the integration test to the image&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; the test using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN xvfb-run qgis_testrunner.sh {insert_your_test_script_filename}&lt;/code&gt; &lt;em&gt;without&lt;/em&gt; the .py
file extension!&lt;/li&gt;
  &lt;li&gt;Sit back and observe all the black magic unfold.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;qgis-is-awesome&quot;&gt;QGIS is awesome&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;https://qgis.org/&quot;&gt;QGIS&lt;/a&gt;, as a (the?) premier GIS client software - is one of my favorite open source projects. Not
just because I’m a geospatial developer/researcher but also because I have seen the fantastic development it has gone
through over the years. It gained a ton of features and a lot more stability along the way. Not that there’s no rough
edges, but rough edges are in &lt;em&gt;any&lt;/em&gt; piece of software if you dig deep enough.&lt;/p&gt;

&lt;p&gt;Although I have been a QGIS user for quite some time, although I never acquainted myself with all the nooks and crannies
of the package. However I recently followed an (internal) masterclass on using the
&lt;a href=&quot;https://www.qgistutorials.com/en/docs/automating_map_creation.html&quot;&gt;Atlas&lt;/a&gt; feature that lets you auto-generate
templated fancy cartographic layouts of your data. Nowadays I’m more of a “what did I actually produce using this
{script,query,export}?” user of QGIS, but I did extensive mapping and editing previously and I found it an extremely
useful tool for the job. Particularly helpful for example is
the &lt;a href=&quot;https://www.qgistutorials.com/en/docs/3/digitizing_basics.html#procedure&quot;&gt;setting where new polygon parts overlapping with already present objects can be automatically clipped (see tutorial part 15-17)&lt;/a&gt;
, then select “Avoid overlap on active layer”:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/qgis/qgis-avoid-overlap-on-active-layer.png&quot; alt=&quot;Avoid overlap on active layer digitizing settings&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For heavy digitisers, this option will save you hours of work and delivers clean maps with non-overlapping polygons.&lt;/p&gt;

&lt;p&gt;But I digress.&lt;/p&gt;

&lt;h1 id=&quot;qgis-plugin-development&quot;&gt;QGIS plugin development&lt;/h1&gt;

&lt;p&gt;My Python skills have come to fruition only relatively recently over the past couple of years, and pure out of curiosity
I started doing some research on what the current state of Python plugin development for QGIS is at the moment. I
started with a simple goal: just to learn some stuff about QGIS plugins in Python and have some fun along the way (
mission: accomplished), but I also decided not to back down on the more complicated stuff. I created a plugin
called &lt;a href=&quot;https://github.com/reinvantveer/namari&quot;&gt;Namari&lt;/a&gt; that allows you to
do &lt;a href=&quot;https://en.wikipedia.org/wiki/Anomaly_detection&quot;&gt;anomaly detection&lt;/a&gt; on geospatial data.&lt;/p&gt;

&lt;p&gt;So, I took to
the &lt;a href=&quot;https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/plugins.html#writing-a-plugin&quot;&gt;docs&lt;/a&gt; which
recommends using the &lt;a href=&quot;https://plugins.qgis.org/plugins/pluginbuilder3/&quot;&gt;plugin builder&lt;/a&gt; to get started. I must say that
this plugin builder is phenomenal to get started with creating your own plugins. It bootstraps an absolute ton of
resources for you, but perhaps a little on the heavy side - it’s easy to get overwhelmed by the amount. I also created
some &lt;a href=&quot;https://github.com/qgis/QGIS-Enhancement-Proposals/issues/223&quot;&gt;level of discussion on the current state of this plugin builder&lt;/a&gt;
.&lt;/p&gt;

&lt;p&gt;Now, I’ve become something of a testing enthusiast over the years. I think it has to do a lot with both research
reproducibility and product stability. Tests allow you to formally verify that something does what it is expected to do,
which often makes me wonder why so little unit/integration testing is done in research.&lt;/p&gt;

&lt;p&gt;It probably has a lot to do with the fact that writing good tests is very hard. I can tell, because I do a lot of it.
But if it were &lt;em&gt;only&lt;/em&gt; hard, I wouldn’t be doing it. Writing tests offers me a level of satisfaction that I cannot reach
from writing working software alone. Testing doesn’t offer &lt;em&gt;every&lt;/em&gt; guarantee: your software is stable only to the level
of tests can verify and then it still doesn’t protect from misuse. But even more than some level of proven correctness,
I truly believe testing can be fun, because of the main goals I set for myself: just to learn stuff.&lt;/p&gt;

&lt;p&gt;And boy does testing stuff get you to learn stuff. I’d almost say that writing tests is maybe the single most effective
way of learning about library APIs and the inner workings of your own code doodles. None more so than for writing
integration tests for QGIS plugins. So here we go.&lt;/p&gt;

&lt;h1 id=&quot;qgis-plugin-integration-testing&quot;&gt;QGIS plugin integration testing&lt;/h1&gt;

&lt;p&gt;Doing unit testing on parts of the plugin is all fine and dandy, but I wanted to do a little bit more than just that.
After all, a QGIS plugin often has some kind of interaction with the graphical user interface of QGIS. So, how do we
test this interaction?&lt;/p&gt;

&lt;p&gt;The answer is through integration testing with a working QGIS instance. Now this is where things get quite complex. QGIS
offers a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis&lt;/code&gt; python package installed in the site-packages of the Python version used by QGIS. But importing this in
a stand-alone Python script doesn’t get you a fully working QGIS instance, it’s just a reference to a module that’s
intended to run as an &lt;em&gt;embedded Python process&lt;/em&gt;. So, this process needs to be tied to a running QGIS instance if you
want to make use of its full range of capabilities.&lt;/p&gt;

&lt;h3 id=&quot;the-iface-object&quot;&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object&lt;/h3&gt;

&lt;p&gt;Central in the understanding of the connection between the Python &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis&lt;/code&gt; namespace and the QGIS app is the notion of
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt;. This &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object can be accessed from a running QGIS Python console (just press Ctrl-Alt-P to start
one):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/qgis-python-console-iface-object.png&quot; alt=&quot;The `iface` object in a running QGIS Python console&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object gives you an instance of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis.gui.QgisInterface&lt;/code&gt;, which kind of is the bridge between a plugin and
events in QGIS. This is why practically all QGIS Python plugins are instantiated by passing (or you could say
dependency-injecting) this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object into the plugin constructor.&lt;/p&gt;

&lt;p&gt;But you’re getting none of this &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; goodness just by unit testing. The plugin builder side-steps this problem by
offering
a &lt;a href=&quot;https://github.com/g-sherman/Qgis-Plugin-Builder/blob/master/plugin_templates/shared/test/qgis_interface.py#L37&quot;&gt;stubbed QGIS interface object&lt;/a&gt;
, but of course this doesn’t do much unless you re-create a lot of the functionality here yourself. But that is both
tedious and unhelpful: we want to validate the workings of our plugin in a &lt;em&gt;real&lt;/em&gt; QGIS environment, not in a stub. How
are we going to check whether it will work in the wild otherwise?&lt;/p&gt;

&lt;p&gt;The hunt for this elusive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object is documented on several
places: &lt;a href=&quot;https://gis.stackexchange.com/questions/392254/pyqgis-iface-object-for-layer-event-handling-in-qgis-plugin-testing&quot;&gt;here is my own&lt;/a&gt;
, but others have hit this as well &lt;a href=&quot;https://github.com/qgis/QGIS-Documentation/issues/3776&quot;&gt;here&lt;/a&gt;
and &lt;a href=&quot;https://gis.stackexchange.com/questions/380678/alternative-of-iface-for-standalone-pyqgis-application&quot;&gt;here&lt;/a&gt;. After
a lot of searching and trying, I managed to come up with a solution that will work almost everywhere.&lt;/p&gt;

&lt;h3 id=&quot;ingredient-number-one-a-dockerfile&quot;&gt;Ingredient number one: a Dockerfile&lt;/h3&gt;

&lt;p&gt;For quite some time now, I have been using Docker as a very helpful development tool. Docker isn’t just handy for
packaging and shipping, it’s also very versatile as part of a development toolchain. The reason is that it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;allows you to build on your own or others’ previous work, the whole standing on shoulders of giants (including your
own :) thing&lt;/li&gt;
  &lt;li&gt;allows you to mess up you build environment &lt;em&gt;inside an isolated environment&lt;/em&gt; without having to deal with the fallout
of a broken development system. If your development environment inside Docker made a mess, then just rinse and repeat
without any sweat&lt;/li&gt;
  &lt;li&gt;allows you to make reproducible builds much more easily. If you can get it to work on your local Docker env, whether
you built on Linux, Mac or Windows, it will probably work on something like GitHub
actions/Travis/Jenkins/TeamCity/Gitlab CI/CircleCI or whatever CI system you’re using. There’s no self-respecting CI
system that doesn’t support Docker.&lt;/li&gt;
  &lt;li&gt;allows you to just remove the test image you built from your local machine and get rid of &lt;em&gt;every last bit&lt;/em&gt; of
toolchain you needed for your project and leave with a clean local system. I cannot tell you how many times I screwed
up my machine setting up all kinds of weird build toolchains that started to get into version conflicts and having no
clue on how to untangle things other than just to uninstall the whole lot (or worse, just flush my machine OS).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this case I think it’s definitely worth using the
official &lt;a href=&quot;https://hub.docker.com/layers/qgis/qgis/latest/images/sha256-31926216a3bea81550a63accd5787592ddd602386ac5355a8e0a32e3b69c7385?context=explore&quot;&gt;QGIS latest Docker image&lt;/a&gt;
for the integration testing. It has all the assets you need to get started designing integration tests. You could use
your own local QGIS installation but the trouble is of course that when you copy or remove the wrong files or to the
wrong location, you end up with a broken QGIS (or worse). So, for development and experimentation, I find a Docker image
to be ideal. So: start your own Dockerfile with&lt;/p&gt;
&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; qgis/qgis:release-3_18  &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or whatever version you&apos;re targeting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can even parameterize the release version, so you can test your plugin against multiple QGIS versions without having
to install them:&lt;/p&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;ARG&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; VERSION&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; qgis/qgis:release-$VERSION  &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or whatever version you&apos;re targeting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and then build using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build -t my_plugin:newer --build-arg VERSION=3_18&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;ingredient-number-two-a-special-integration-test-script&quot;&gt;Ingredient number two: a special integration test script&lt;/h2&gt;

&lt;p&gt;Second aspect of our integration test toolchain is a special python script that can be executed by
a &lt;a href=&quot;https://github.com/qgis/QGIS/blob/master/.docker/qgis_resources/test_runner/qgis_testrunner.sh&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis_testrunner.sh&lt;/code&gt;&lt;/a&gt;
 script that is present in the QGIS Docker image by default. You can call your python script anything you want, but
the minimal requirement is that:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;it can be found by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis_testrunner.sh&lt;/code&gt; shell script,&lt;/li&gt;
  &lt;li&gt;that it has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;run_all()&lt;/code&gt; function (having no parameters, returning nothing) and&lt;/li&gt;
  &lt;li&gt;that at some point it emits a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ran&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OK&lt;/code&gt; through standard out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most simple test script that can be ran by the test runner inside the docker image is thus:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python3&quot;&gt;def run_all():
    print(&apos;Ran OK&apos;)

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;However, this test script is capable to do a lot more: it is able to instantiate our plugin with an actual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt;
interface to the QGIS application!&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;qgis&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;namari&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;classFactory&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Hello QGIS!&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;plugins&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plugins&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iface&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;This test needs an actual iface interface to QGIS&apos;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;namari&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;classFactory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;utils&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;iface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Ran OK&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So, this script will not only verify that we have a &lt;em&gt;real&lt;/em&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object instead of a stubbed or empty one, but it will
also instantiate our plugin (called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;namari&lt;/code&gt;) to hook directly into the QGIS application. For comparison: you can
import &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis.utils&lt;/code&gt; at any time from any python script, but the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iface&lt;/code&gt; object will be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None&lt;/code&gt; unless you
import it inside a running QGIS application - or from this  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis_testrunner.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now that we have the beginnings of our Python integration test script, we can call it using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis_testrunner.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We expand our Dockerfile by:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;copying our plugin,&lt;/li&gt;
  &lt;li&gt;using &lt;a href=&quot;https://pypi.org/project/pb-tool/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pb_tool&lt;/code&gt;&lt;/a&gt; to deploy the plugin files to the QGIS plugins dir,&lt;/li&gt;
  &lt;li&gt;add the plugins dir to the PYTHONPATH and&lt;/li&gt;
  &lt;li&gt;call the test script&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; qgis/qgis:release-3_18&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# You should consider using pipenv for dependency management&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;pb_tool

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; plugin&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Copy all your plugin files to the plugin working directory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; . ./&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Put the rather long path to the standard plugins dir in a PLUGIN_DIR environment variable for re-use&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; PLUGIN_DIR=/QGIS/build/output/python/plugins&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Deploy plugin to standard plugins dir using pb_tool&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pb_tool deploy &lt;span class=&quot;nt&quot;&gt;--config_file&lt;/span&gt; pb_tool_docker.cfg &lt;span class=&quot;nt&quot;&gt;--plugin_path&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$PLUGIN_DIR&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-confirm&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Run the integration tests by using the test script&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; PYTHONPATH=$PYTHONPATH:/namari/test:$PLUGIN_DIR&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;xvfb-run qgis_testrunner.sh integration_test

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qgis_testrunner.sh&lt;/code&gt; will run your integration tests!&lt;/p&gt;
</description>
        <pubDate>Sat, 10 Apr 2021 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2021/04/10/qgis-plugin-development.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2021/04/10/qgis-plugin-development.html</guid>
        
        
      </item>
    
      <item>
        <title>SQL as programming language? Better not.</title>
        <description>&lt;p&gt;The past few months I have been given the opportunity to work with a prototype built in and around PostGreSQL. In this post, I intend to share some of the experiences with application programming in SQL and the accompanying procedural languages for use inside a database (such as pl/pgsql). I’d like to stress that the opinion expressed here in no way is pointed toward any individual. Instead, it’s about technology and how I feel towards its implementations.&lt;/p&gt;

&lt;h1 id=&quot;tldr&quot;&gt;TL;DR:&lt;/h1&gt;
&lt;p&gt;In short: use SQL for persistence: fetching data from, or writing to a database, but don’t use it for application logic.&lt;/p&gt;

&lt;p&gt;It’s perfectly fine to prototype your application in any kind of programming language, including SQL. SQL has some extremely concise expressivity that can make a data transformation pipeline very compact. However, be wise and:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Provide every bit of information on &lt;strong&gt;why&lt;/strong&gt; each SQL statement or clause is doing what it’s doing&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Drop the prototype as soon as you start thinking about going to production&lt;/strong&gt;. It will not be worth the effort to put a prototype into production. Don’t ever be tempted into thinking it will perform, be stable or even work in the first place: it is unlikely to work out in your (financial) benefit.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Use a &lt;strong&gt;generic programming language&lt;/strong&gt;, such as Python, Java, JavaScript, C#, for your application logic and &lt;strong&gt;don’t&lt;/strong&gt; stick any of it in SQL. Use any programming language that lets you:&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;Write code in a concise and &lt;em&gt;readable&lt;/em&gt; way. Long SQL queries tend to become utterly incomprehensible for anyone except the writer of the code. There’s no generally agreed upon code style and no linter for SQL that will help you format the code in a way that makes it compliant to a standard or easier to understand for fellow colleagues.&lt;/li&gt;
      &lt;li&gt;Most importantly of all: write tests for your code. Testing application logic in SQL is hard and expensive because it &lt;strong&gt;has no built-in testing framework&lt;/strong&gt;. Use a generic programming language instead for any application logic you need.&lt;/li&gt;
      &lt;li&gt;Do proper dependency/package management to specify what your application logic needs to work in production. Install dependencies and packages that you trust and that are thoroughly tested. For many open source dependencies there are built-in testing pipelines in place where you can see how your dependencies are tested.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;a-very-short-introduction-into-the-purposes-of-sql&quot;&gt;A very short introduction into the purposes of SQL&lt;/h1&gt;
&lt;p&gt;SQL is a language for interacting with a database. It is a set-based language that operates primarily on table-level. In this sense, the set that a SQL query operates on, is an entire set of records in one or more tables over which the SQL ‘loops’ automatically. Especially in querying different data tables, SQL is very good at integrating, filtering and summarizing data. It is then also very good at writing the results of this operation back into a new table. This is often referred to as an ETL process: an Extract (load data from tables), Transform (filter the data, summarize), and Load (write results into a new table).&lt;/p&gt;

&lt;p&gt;The database here acts as a strategy for “persistence”: a way to store data that is not lost when a machine restarts. It is a popular method, because the way a database organizes lots of data is much more performant than the default persistence method, namely a file system. A file system generally is a tree-like system of folders that contain either subfolders and/or file. You may have a “Documents” folder, containing “Projects”. This “Projects” folder may have a “Git” folder, to designate project documents that are tracked in a version control system called Git. This “Git” folder then may then contain the subfolder “web_app_for_incredible_solutions” or some other project, etcetera. The file system therefore often expresses a tree of “things” that go from generic to specific, but this tends to gets swamped if you have a lot of fine-grained data.&lt;/p&gt;

&lt;p&gt;Databases have a lot fewer levels than paths in your filesystem do. There is (such as for PostGreSQL databases) the Database level, the Schema level and then there are Tables which have Rows and Columns. This is the order of significance: you have a database for your department, with perhaps a schema for each customer (or you may have a database for each customer) or perhaps for each application or for a particular data set. Each schema will probably contain tables. Often, you name the table after a plural of the thing type it contains. A record in a Table often represents a real-world or imagined “thing”: an invoice, a car, a car brand, a building, a customer, etcetera. Each field will express some kind of property or attribute of the object in the record. These are the basis building blocks of a database.&lt;/p&gt;

&lt;p&gt;This is why simple SQL queries often make a lot of sense, just by looking at them. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT customer_name FROM customers;&lt;/code&gt; will evidently get you all names of your customers from a set containing all customers for a given schema (the name of which is the default schema here).&lt;/p&gt;

&lt;h1 id=&quot;where-the-trouble-starts&quot;&gt;Where the trouble starts&lt;/h1&gt;
&lt;p&gt;There’s trouble with SQL on many fronts that hamper production application development.&lt;/p&gt;

&lt;h2 id=&quot;readability&quot;&gt;Readability&lt;/h2&gt;
&lt;h3 id=&quot;sql-as-a-human-readable-language&quot;&gt;SQL as a human-readable language&lt;/h3&gt;
&lt;p&gt;The simplicity of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT {attribute} FROM {thing_type};&lt;/code&gt; has earned SQL its reputation as an easy to read language. This is more than just a nice idea. Programming is primarily of use as a method for communication: from humans to other humans, that is. It gets read by programmers after you write it, so people after you need to know what and why your program does what it does. This is no different for SQL.&lt;/p&gt;

&lt;p&gt;However, the readability of SQL quickly evaporates when the SQL query gets longer and more syntax is used. There is very little readable left in queries that&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Use complicated and multiple join types such as &lt;a href=&quot;https://www.postgresqltutorial.com/postgresql-full-outer-join/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL OUTER JOIN&lt;/code&gt;&lt;/a&gt;, or &lt;a href=&quot;https://www.postgresqltutorial.com/postgresql-cross-join/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CROSS JOIN&lt;/code&gt;&lt;/a&gt;. Even after reading the tutorials, I don’t know whether a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL JOIN&lt;/code&gt; will return something different than a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FULL OUTER JOIN&lt;/code&gt;. If it’s optional and if it does return the same result set, then why is it even there? If the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CROSS JOIN&lt;/code&gt; doesn’t do anything than just the cartesian product of tables, then why does it exist? What exactly is “crossy” about a cross join?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Mimic control flow, such as &lt;a href=&quot;https://www.postgresqltutorial.com/postgresql-coalesce/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COALESCE&lt;/code&gt;&lt;/a&gt;. To “coalesce” means to merge or grow into one another. But that is not what this function does: it takes the first non-null value from a list of values passed to the function. What is wrong about having called this function just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FIRST_NON_NULL()&lt;/code&gt;?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Use many, many nested functions. This is often the case, since you can’t assign to an intermediate or temporary variable. You can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WITH&lt;/code&gt; clauses to work around this, but this will generally reward the reader with an endless stream of WITH blocks that only vaguely resemble a procedural flow.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, use a programming language to write out the application logic in an easy to understand way. I find Python to be ideal for this purpose. Yes, it’s possible to make an unreadable mess in any programming language, but Python enables you to write readable code unlike any other programming language does. It doesn’t &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COALESCE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CROSS JOIN&lt;/code&gt; and so shouldn’t you. Write readable code and douse with plenty of comments &lt;strong&gt;why&lt;/strong&gt; you do it that particular way. Don’t us endless nesting of functions that SQL pushes you towards, but break up meaningful pieces of procedural code into functions and execute them one after another. Use classes if they improve the readability of your code: SQL can’t.&lt;/p&gt;

&lt;p&gt;So, the supposed readability benefit for SQL is mostly voided once you get to more advanced SQL queries. It is a language with a lot of syntax, just as most other programming languages. Readability automatically brings us to perhaps the most difficult part of programming in general: complexity and testing.&lt;/p&gt;

&lt;h3 id=&quot;code-style&quot;&gt;Code style&lt;/h3&gt;
&lt;p&gt;Unlike other commonly used programming languages, SQL does not have a generally agreed upon code style. Generally, programming languages make heavy use of style guides to keep code legible. There is &lt;a href=&quot;https://coryrylan.com/blog/why-enforcing-code-style-is-important&quot;&gt;some level of debate&lt;/a&gt; on whether code style is really necessary, but most coders seem to agree that it is a good idea, specifically for situations where code is shared between people. JavaScript coders often use something like the &lt;a href=&quot;https://google.github.io/styleguide/jsguide.html&quot;&gt;Google style standard&lt;/a&gt; or the &lt;a href=&quot;https://github.com/airbnb/javascript&quot;&gt;AirBnB standard&lt;/a&gt;, There’s &lt;a href=&quot;https://www.python.org/dev/peps/pep-0008/&quot;&gt;PEP 8&lt;/a&gt; for Python, Java also has a &lt;a href=&quot;https://google.github.io/styleguide/javaguide.html&quot;&gt;Google style guide&lt;/a&gt; and the list goes on. Every single one of these style guides comes with some kind of tool that helps you check for style errors: a &lt;a href=&quot;https://en.wikipedia.org/wiki/Lint_(software)&quot;&gt;linter&lt;/a&gt;. Usage of these linters is essential to keep track of all style errors in your code, because it’s generally impossible to keep track of all the different kinds of style rules.&lt;/p&gt;

&lt;p&gt;There’s a &lt;a href=&quot;https://duckduckgo.com/?q=sql+code+style&quot;&gt;few websites out there&lt;/a&gt; that propose SQL formatting, but it’s also clear that there is no SQL style formatter or checker - a linter. I worked with someone who used &lt;a href=&quot;https://sqlformat.org/&quot;&gt;this site&lt;/a&gt; to format his SQL, but having to copy-paste your code between tools is no more than an ad-hock workaround, not a solution. The &lt;a href=&quot;https://about.gitlab.com/handbook/business-ops/data-team/platform/sql-style-guide/&quot;&gt;GitLab SQL style guide&lt;/a&gt; stresses this lack of linters, to where it becomes a “collective responsibility” to enforce it. The lack of SQL style linters is a little surprising, seeing the vast amount of programming being done in SQL. Generally, most programming languages are used in IDEs that provide style hints that prevent you from typing errors and typos, but I have little evidence so far that these are in any frequent use. This means that a SQL programmer can apply basically any kind of formatting or lack thereof to sql. For example, there’s no agreed upon standard for using CAPITALIZATION (a practice that seems quite old-fashioned to me, and rather shouty). But also, since SQL often makes extensive use of function call nesting, this is detrimental to the readability of longer SQL code blocks. This, to me, very much points to SQL as being a pseudo-programming language.&lt;/p&gt;

&lt;h2 id=&quot;complexity-and-testing&quot;&gt;Complexity and testing&lt;/h2&gt;
&lt;p&gt;Once SQL queries start to get longer, an uncanny feeling starts to creep up on me. Why is there so much code needed to get some stuff from the database? Is the structure of the database that complex?&lt;/p&gt;

&lt;p&gt;Often, however, I see that the SQL code is not only fetching things from the backend or writing data to it. Often, it applies some kind of transformation. It performs part of the application logic that the client code is supposed to do. I hear my colleagues explain: “It’s much easier this way.” Yes, it’s easier for the one writing the query because he/she designed it. There was no complicated testing procedure involved. There is a simple reason for this: SQL does not have a testing framework.&lt;/p&gt;

&lt;p&gt;Please allow me let this sink in. &lt;em&gt;SQL does not have a testing framework&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now, let me explain why this is such a shocking statement to make. It means that SQL has no way of checking the outcome of a SQL statement in a basic validation strategy that comes with every single self-respecting programming language around. Even &lt;a href=&quot;https://github.com/neopragma/cobol-unit-test&quot;&gt;COBOL has a unit testing framework&lt;/a&gt;, for crying out loud. What makes SQL script writers think they don’t need it?&lt;/p&gt;

&lt;p&gt;One of the most fundamental properties that separate production code from hacking, is tests. Without a testing framework, SQL encourages hacking around, rather than writing tested production code. Putting SQL application code into production without tests is simply nuts.&lt;/p&gt;

&lt;p&gt;Imagine telling Java, or C#, or Python developers that all the testing frameworks they were used to working with, were removed and that they were no longer allowed to write tests in their beloved and preferred method. Imagine telling them that they would have to resort to &lt;em&gt;a different language&lt;/em&gt; to express their tests in. That they would have to build their application logic, and then inject the functions into that testing language to see whether or not it produced the right outcome. Really.&lt;/p&gt;

&lt;p&gt;But this is the reality for SQL. It’s not SQL in itself, of course, it shares this unfortunate trait with some other languages: there’s&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;bash (&lt;a href=&quot;https://en.wikipedia.org/wiki/Bash_(Unix_shell)&quot;&gt;Bourne again shell&lt;/a&gt;) scripting: a &lt;a href=&quot;https://stackoverflow.com/questions/1339416/unit-testing-bash-scripts&quot;&gt;language without a testing framework&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;regex (&lt;a href=&quot;https://en.wikipedia.org/wiki/Regular_expression&quot;&gt;regular expression&lt;/a&gt;) doesn’t come with a unit testing framework. This sounds quite obvious, but it also explains why I start frowning in the presence of any regular expression containing more than four characters. Much more than that, and they’re only readable for those who wrote them. Also: if you use them, they need to be tested in any language other than regex that &lt;em&gt;does&lt;/em&gt; have a testing framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the comments in the links it’s clear that it’s not impossible to test bash scripts, regular expressions and SQL. The point is, that considering that these languages do not have testing possibilities makes it much more likely that they don’t. There may be testing frameworks for bash, but the chance that you will come across anyone ever having used the proposed frameworks is extremely unlikely. The same goes for SQL. How many people have you met that employ a testing framework for SQL scripts?&lt;/p&gt;

&lt;p&gt;Instead, for SQL, bash and strictly speaking regex, you need something called “integration tests” to validate their correct workings. Regex is often built into the programming language used for testing, so you’ll find few regular expressions ‘out in the wild’ without some kind of programming language to use them. But for bash and SQL, this is different. You need an external dependency to test them. This makes them integration tests: the tests need to integrate separate components or applications into a requirements or behavior test. For bash this isn’t much of a dependency: you need a Linux system. For SQL, you need an entire database, running. Probably on a different machine than the one you’re running your tests on. It’s a systems integration test!&lt;/p&gt;

&lt;p&gt;It gets worse for SQL. Many SQL scripts are action queries that alter the state of the database, and often requiring schemas and tables to pull data from. This makes these queries expensive to test: not only do you need to test to validate the requirements of the output, you also have to insert test data as well. Since the actions in the SQL script affect the state of the database, you don’t want your test to contaminate the production databas. So, you need some kind of test database. If you don’t have any such thing, it is improbable that you’re going to test your queries. Once you’re done testing, you need to remove the test assets from the test database as well to restore it back to a clean state. This means you’re going to have to write functions to inject and restore database state, that you will probably have to test. Still feeling the desire to test queries, are you?&lt;/p&gt;

&lt;p&gt;In short: please do test your queries in a test database, but keep application logic from SQL. Testing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT * FROM customers;&lt;/code&gt; is hardly necessary or complicated. This is no longer the case once you start moving application logic inside of your queries, because now you &lt;em&gt;will&lt;/em&gt; have to write tests for them. Make sure you do. Saying “last month my query was fine,” is useless because last month doesn’t matter if your application crashes now. Having test means making sure that under any circumstance and at any time, you are able to validate the correct workings of your code. Doing some manual testing now and again and calling “But it worked on my machine!” is a recipe for disaster. Therefore: put as little application or ‘business’ logic in your queries as possible, because testing for meeting all the requirements is costly for SQL. Instead, write you application logic in a proper programming language: it’s much easier and therefore cheaper to write unit tests for ‘generic’ programming languages.&lt;/p&gt;

&lt;h2 id=&quot;dependency-management&quot;&gt;Dependency management&lt;/h2&gt;
&lt;p&gt;Every self-respecting programming language nowadays has a dependency management system. JavaScript has NPM, Java has Maven, C# has NuGet, and Python has Pipenv. No programming language is complete without some kind of package management system. &lt;strong&gt;SQL has none&lt;/strong&gt;. You cannot express in a suite of SQL scripts, a SQL project if you like, what dependencies it should or shouldn’t require. That means that the code is possibly going to break in unknown places  once (and at some time you will) upgrade your database.&lt;/p&gt;

&lt;h3 id=&quot;version-dependent-code&quot;&gt;Version-dependent code&lt;/h3&gt;
&lt;p&gt;The more usage of database extensions and procedural logic there is in your SQL, the higher the chance that your SQL will break. There are at least 3 current versions of PostGIS, the geospatial extension for PostGIS, with one major version difference, from major version 2 to 3. Multiply this by about 4 major PostGreSQL database versions and you have a combination of backend components for which your query may or may not work. There is a special &lt;a href=&quot;https://postgis.net/docs/manual-3.0/PostGIS_FAQ.html#legacy_faq&quot;&gt;legacy FAQ&lt;/a&gt; for the trouble that upgrading from major version 1 to 2 gave. There are many dependency pitfalls in PostGIS - consider the &lt;a href=&quot;https://postgis.net/docs/manual-3.0/reference.html#reference_sfcgal&quot;&gt;SFCGAL functions&lt;/a&gt; that require CGAL to be present at build-time. That means that if you upgrade your database but forget to install CGAL, part of your code is going to break.&lt;/p&gt;

&lt;p&gt;Dependency management is of course a problem in any system, but the point is that programming languages tend to work with them very diligently. All major programming languages have options to specify what packages you want, versus the (sub)packages versions that your application &lt;em&gt;requires&lt;/em&gt; to work. This requirement is often captured in a “lockfile” of some kind. For Python with Pipenv, this is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pipfile.lock&lt;/code&gt;. For JavaScript and NPM, this is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package-lock.json&lt;/code&gt;. For Ruby and Gem, it’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt;. For C# and NuGet, it’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;project.lock.json&lt;/code&gt;. The list goes on, but it’s clear that the pseudo-programming languages such as SQL, bash and regex don’t have these. They don’t have package managers at all, and you can argue that bash and regex don’t need them. This is clearly not the case for database extensions, so you’re on your own, especially when using database extensions.&lt;/p&gt;

&lt;p&gt;The upshot of this is that you have no guarantees on whether code in SQL will keep working in production if you start upgrading databases or other database dependencies such as PostGreSQL extensions. Again, if you simply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT * FROM customers&lt;/code&gt; this is highly unlikely to pose a problem. But the more business and application logic you put into your SQL, and the more extensions you use, the greater the risk is going to be. In a worst-case scenario, you won’t be able to upgrade your database until you’ve rewritten every application you have in production.&lt;/p&gt;

&lt;p&gt;Having said this, I have yet to encounter a SQL script that states in which version of what database it is supposed to work, and which extensions are required. Please move your application logic to a generic programming language ASAP.&lt;/p&gt;

&lt;h3 id=&quot;battle-tested-dependencies&quot;&gt;Battle-tested dependencies&lt;/h3&gt;
&lt;p&gt;A second reason why most programming languages make heavy use of package management, is because it allows you to re-use code that has been tried and tested by many users before. Most package management systems gather some form or other of popularity statistics, allowing you to see how many users have successfully used a dependency.&lt;/p&gt;

&lt;p&gt;Since SQL doesn’t have a package manager, it’s up to you to pick code examples from the web, wherever you may find them. This propagates a cut-and-paste mentality, for example from sources as Stack Overflow. Now let me be precise: my work wouldn’t be possible without &lt;a href=&quot;https://stackoverflow.com/&quot;&gt;Stack Overflow&lt;/a&gt;, it’s essential for finding clues on how to find solutions for coding problems that I find hard to solve. However, Stack Overflow isn’t a dependency management system. When you start copy-pasting accepting answers as accepted solutions for coding problems, it doesn’t allow you to skip writing tests for the part you copied. The code may have been for a problem &lt;em&gt;like&lt;/em&gt; yours, but it’s probably contextual. To make sure that this context applies to your situation as well, you normally write a test to verify. Without a testing framework, SQL lures you into bad coding practices of copying accepted answers from forums at face value.&lt;/p&gt;

&lt;p&gt;Bottom line: copy all you want from forums, but then write a test in language different from SQL to verify that the SQL solution applies for your specific problem and every requirement in encompasses.&lt;/p&gt;
</description>
        <pubDate>Sun, 19 Jul 2020 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2020/07/19/sql-as-programming-language.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2020/07/19/sql-as-programming-language.html</guid>
        
        
      </item>
    
      <item>
        <title>Elliptical Fourier analysis</title>
        <description>&lt;p&gt;Recently, I’ve become quite interested in Fourier series for the purposes of geospatial shape analysis. In particular, I’ve spent quite some time getting to grips with an elliptical variant of the well-known Fourier transform family, a variant based on calculating the coefficients of ellipses. Using a predefined number of ellipses, any closed contour can be described using the parameters of these ellipses. Once created, the parameters can be used to reconstruct an approximation of the original shape.&lt;/p&gt;

&lt;p&gt;I created a web demo to demonstrate its workings. The demo is primarily based on &lt;a href=&quot;https://leafletjs.com/&quot;&gt;leaflet&lt;/a&gt; for the geospatial part, and &lt;a href=&quot;https://www.tensorflow.org/js&quot;&gt;tensorflow.js&lt;/a&gt; for the coefficient operations. That these libraries exist in free, open source form is truly marvellous. The demo is best viewed on a desktop environment.&lt;/p&gt;

&lt;iframe src=&quot;https://spinlab.github.io/neighborhoods-autoencoder/js/dist/index.html&quot; style=&quot;height: 700px; width: 100%&quot;&gt;&lt;/iframe&gt;

&lt;h1 id=&quot;how-did-we-get-here&quot;&gt;How did we get here?&lt;/h1&gt;
&lt;p&gt;Fourier transformation is the deconstruction of a signal into its main constituent frequencies. Or in other words: mapping from (often) a time or space domain to a frequency domain, as in the following animation:&lt;/p&gt;

&lt;p&gt;&lt;a title=&quot;Lucas V. Barbosa [Public domain], via Wikimedia Commons&quot; href=&quot;https://commons.wikimedia.org/wiki/File:Fourier_transform_time_and_frequency_domains.gif&quot;&gt;&lt;img width=&quot;512&quot; alt=&quot;Fourier transform time and frequency domains&quot; src=&quot;https://upload.wikimedia.org/wikipedia/commons/5/50/Fourier_transform_time_and_frequency_domains.gif&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Animation of signal decomposition into main frequencies&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let’s start in the early 19th century, with &lt;a href=&quot;https://en.wikipedia.org/wiki/Joseph_Fourier&quot;&gt;Jean-Baptiste Joseph Fourier&lt;/a&gt; (1768-1830), the man who gave his name to the Fourier transform. Fourier contributed in his 1820’s work &lt;a href=&quot;https://books.google.nl/books?id=TDQJAAAAIAAJ&amp;amp;dq=Th%C3%A9orie%20analytique%20de%20la%20chaleur&amp;amp;hl=nl&amp;amp;pg=PR3#v=onepage&amp;amp;q=Th%C3%A9orie%20analytique%20de%20la%20chaleur&amp;amp;f=false&quot;&gt;&lt;em&gt;Théorie analytique de la chaleur&lt;/em&gt;&lt;/a&gt; the notion that any mapping function of a variable could be expressed as a &lt;a href=&quot;https://en.wikipedia.org/wiki/Fourier_series&quot;&gt;&lt;strong&gt;Fourier series&lt;/strong&gt;&lt;/a&gt; (this might be an infinite ): a series of waves of multiples (&lt;a href=&quot;https://en.wikipedia.org/wiki/Harmonic&quot;&gt;harmonics&lt;/a&gt;) of the variable itself.&lt;/p&gt;

&lt;p&gt;&lt;a title=&quot;Moodswingerscale.jpg: Y Landman derivative work: W axell [Public domain], via Wikimedia Commons&quot; href=&quot;https://commons.wikimedia.org/wiki/File:Moodswingerscale.svg&quot;&gt;&lt;img width=&quot;512&quot; alt=&quot;Moodswingerscale&quot; src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/2/2f/Moodswingerscale.svg/512px-Moodswingerscale.svg.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Seven harmonics of a signal&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;fourier-series-definition&quot;&gt;Fourier series definition&lt;/h2&gt;
&lt;p&gt;This series was defined as &lt;em&gt;N&lt;/em&gt; pairs of coefficients (&lt;em&gt;a&lt;/em&gt; and &lt;em&gt;b&lt;/em&gt;) for each ‘harmonic order’ &lt;em&gt;n&lt;/em&gt;, using the trigonomical formulation:&lt;/p&gt;

\[\begin{aligned} a_{n} &amp;amp;=\frac{2}{P} \int_{P} f(x) \cdot \cos \left(2 \pi x \frac{n}{P}\right) d x \\ b_{n} &amp;amp;=\frac{2}{P} \int_{P} s(x) \cdot \sin \left(2 \pi x \frac{n}{P}\right) d x \end{aligned} \tag{1}\]

&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;the harmonic order &lt;em&gt;n&lt;/em&gt; is the number of sine/cosine pair integrals to fit to the signal;&lt;/li&gt;
  &lt;li&gt;\(f(x)\) is some known function producing the signal, mapped to the y-axis. In practice we map this to the y-axis.&lt;/li&gt;
  &lt;li&gt;The variable &lt;em&gt;P&lt;/em&gt; represents the interval (or &lt;em&gt;P&lt;/em&gt; for period) of the signal - the length of the signal we have sampled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few other interesting things here to note:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Each harmonic order &lt;em&gt;n&lt;/em&gt; adds one more level of detail to the analysis.&lt;/li&gt;
  &lt;li&gt;Each harmonic order &lt;em&gt;n&lt;/em&gt; represents &lt;em&gt;n&lt;/em&gt; passes over the period &lt;em&gt;P&lt;/em&gt;&lt;/li&gt;
  &lt;li&gt;The Fourier series decomposition isn’t simply looking for all the frequencies in the signal: in practice it’s looking for frequencies that fit a combination of multiples of period &lt;em&gt;P&lt;/em&gt;. That means that, for some signals, you have to fit a very large number of harmonics to the signal to get good signal characteristics.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;complex-valued-formula&quot;&gt;Complex-valued formula&lt;/h3&gt;
&lt;p&gt;There is another, more common method for expressing the Fourier transform, using complex number notation. According to the definition from &lt;a href=&quot;https://www.wolframalpha.com/input/?i=Fourier+transform&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22transformfunction%22%7D+-%3E%22e%5E(-t%5E2)+sin(t)%22&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22variable1%22%7D+-%3E%22t%22&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22variable2%22%7D+-%3E%22w%22&amp;amp;assumption=%7B%22C%22,+%22Fourier+transform%22%7D+-%3E+%7B%22MathWorld%22%7D&quot;&gt;Wolfram Alpha&lt;/a&gt;, the Fourier transform is a generalized form of &lt;em&gt;complex&lt;/em&gt; Fourier series. Euler’s formula states that:&lt;/p&gt;

\[e^{i x}=\cos x+i \sin x, \tag{2}\]

&lt;p&gt;expressing the relationship between trigonometry and complex numbers. Thus, the sum of sine and cosine values can be expressed as a complex number with real part \(x\) and an imaginary part \(i\). Note the similarity of the cosine and sine parts of the formula with Fourier’s trigonometrical approach. This means that the Fourier transform for &lt;strong&gt;frequency&lt;/strong&gt; \(k\) can be expressed in the parts of a complex number:&lt;/p&gt;

\[F(k)=\int_{-\infty}^{\infty} f(x) e^{-2 \pi i k x} d x \tag{3}\]

&lt;p&gt;&lt;a href=&quot;https://www.wolframalpha.com/input/?i=Fourier+transform&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22transformfunction%22%7D+-%3E%22e%5E(-t%5E2)+sin(t)%22&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22variable1%22%7D+-%3E%22t%22&amp;amp;assumption=%7B%22F%22,+%22FourierTransformCalculator%22,+%22variable2%22%7D+-%3E%22w%22&amp;amp;assumption=%7B%22C%22,+%22Fourier+transform%22%7D+-%3E+%7B%22MathWorld%22%7D&quot;&gt;source: Wolfram Alpha&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;which is also commonly written as&lt;/p&gt;

\[\mathcal{F}(\omega)=\frac{1}{\sqrt{2 \pi}} \int_{-\infty}^{\infty} f(t) e^{-i \omega t} \mathrm{d} t \tag{4}\]

&lt;p&gt;which is substituting x-axis value \(x\) with time variable \(t\), substituting  and factoring out the \(\frac{1}{\sqrt{2 \pi}}\). Also, whe have the angular frequency \(\omega\) as &lt;a href=&quot;https://en.wikipedia.org/wiki/Angular_frequency&quot;&gt;\(\omega=\frac{2 \pi}{T}=2 \pi f\)&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;reconstruction-from-coefficients&quot;&gt;Reconstruction from coefficients&lt;/h2&gt;
&lt;p&gt;The brilliant thing is that this decomposed signal allows inverse transormation as well, to a reconstruction of the signal itself:&lt;/p&gt;

\[s_{N}(x)=\frac{a_{0}}{2}+\sum_{n=1}^{N}\left(a_{n} \cos \left(\frac{2 \pi n x}{P}\right)+b_{n} \sin \left(\frac{2 \pi n x}{P}\right)\right), \tag{5}\]

&lt;p&gt;using the trigonometrical notation. Which boils down to that the reconstruction is the sum of all the harmonic orders reprojected from circular parameters back into cartesian space.&lt;/p&gt;

&lt;h1 id=&quot;discrete-time-fourier-transforms&quot;&gt;Discrete Time Fourier Transforms&lt;/h1&gt;
&lt;p&gt;Fitting Fourier series to perfect mathematical functions is all fine, but in practice we need to fit this series to data that is sampled from an unknown signal function. Remember, from the previous section, that for the standard Fourier series computation, we need access to the full continuous signal mapping function \(f(x)\). However, in many practical cases, we do not have access to the original function - it is usually a sampled signal produced in nature or through some unknown mechanism that we happen to sample.&lt;/p&gt;

&lt;p&gt;This is where the &lt;a href=&quot;https://en.wikipedia.org/wiki/Discrete-time_Fourier_transform&quot;&gt;Discrete Time Fourier transform&lt;/a&gt; comes in:&lt;/p&gt;

\[X_{2 \pi}(\omega)=\sum_{n=-\infty}^{\infty} x[n] e^{-i \omega n} \tag{6}\]

&lt;p&gt;for frequency variable \(\omega\) as the radians per sample, for each integer &lt;em&gt;n&lt;/em&gt;. The advantage here is that we do not need a full integrand of the mapping function &lt;em&gt;f&lt;/em&gt; and the other terms, but we can sum over the individual values of &lt;em&gt;x&lt;/em&gt; over &lt;em&gt;n&lt;/em&gt; harmonics.&lt;/p&gt;

&lt;h3 id=&quot;discrete-fourier-transform&quot;&gt;Discrete Fourier Transform&lt;/h3&gt;
&lt;p&gt;The Discrete Time Fourier Transform is similar the Discrete Fourier Transform (DFT). The DFT “converts a finite sequence of equally-spaced samples of a function into a same-length sequence of equally-spaced samples of the discrete-time Fourier transform” (&lt;a href=&quot;https://en.wikipedia.org/wiki/Discrete_Fourier_transform&quot;&gt;wikipedia&lt;/a&gt;):&lt;/p&gt;

\[\begin{aligned} X_{k} &amp;amp;=\sum_{n=0}^{N-1} x_{n} \cdot e^{-\frac{i 2 \pi}{N} k n} \\ &amp;amp;=\sum_{n=0}^{N-1} x_{n} \cdot[\cos (2 \pi k n / N)-i \cdot \sin (2 \pi k n / N)] \end{aligned} \tag{7}\]

&lt;p&gt;This produces the  &lt;em&gt;k&lt;/em&gt;-th pair for &lt;em&gt;N&lt;/em&gt; complex numbers, the values &lt;em&gt;n&lt;/em&gt; are sampled at equal intervals. Here, we show both the complex-valued form (top) and trigonomical form (bottom) from &lt;a href=&quot;https://en.wikipedia.org/wiki/Discrete_Fourier_transform&quot;&gt;wikipedia: Discrete Fourier Transform&lt;/a&gt; for clarity. So, the Discrete Time Fourier Transform is equal to the Discrete Fourier transform only if our samples are equally spaced.&lt;/p&gt;

&lt;h1 id=&quot;fast-fourier-transform&quot;&gt;Fast Fourier Transform&lt;/h1&gt;
&lt;p&gt;There was however one problem with the standard Fourier transform method: it was computationally rather expensive. It scaled quadratically with the number of harmonics. So, we jump forward in time, to the point where extensive machine computation of Fourier series was becoming quite desirable. So suppose you would want to monitor some nation’s nuclear detonation experiments using sensors outside of their geographical domain, you would have to deal with signals that are probably rife with background noise. There would be a lot of data, and in the 1960’s computer hardware and computation was expensive. Ideally, you would like on-site, real-time processing and possibly other compound words that require dashes.&lt;/p&gt;

&lt;p&gt;So, in 1965 just after the cold-war pinnacle &lt;a href=&quot;https://en.wikipedia.org/wiki/Cuban_Missile_Crisis&quot;&gt;cuban missile crisis&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/James_Cooley&quot;&gt;James Cooley&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/John_Tukey&quot;&gt;John Tukey&lt;/a&gt; together published their &lt;a href=&quot;https://www.ams.org/journals/mcom/1965-19-090/S0025-5718-1965-0178586-1/S0025-5718-1965-0178586-1.pdf&quot;&gt;groundbreaking and much-improved variant on Fourier transformation&lt;/a&gt;. They came up with a computationally more efficient means to arrive at the same coefficients as the standard Fourier transform:&lt;/p&gt;

&lt;div&gt;
    &lt;div style=&quot;float: left; width: 30%; height:&quot;&gt;
        &lt;a href=&quot;https://www.pinterest.com/pin/356347389250590948/&quot;&gt;
            &lt;img height=&quot;167px&quot; src=&quot;https://i.pinimg.com/474x/8f/a7/67/8fa767578f1259018ad1351858299044--math-james-darcy.jpg&quot; /&gt;
        &lt;/a&gt;
    &lt;/div&gt;
    &lt;div style=&quot;float: right; width: 70%; height: 167px&quot;&gt;
        $$
        X_{k}=\sum_{n=0}^{N-1} x_{n} e^{-i 2 \pi k n / N} \tag{8}
        $$
    &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;for which they only needed four and a half pages of perfectly legible article to explain their innovation. Their work was so profound that it has been called (one of ) the most important numerical formula(s) (&lt;a href=&quot;https://en.wikipedia.org/wiki/Fast_Fourier_transform&quot;&gt;see top of wikipedia page&lt;/a&gt;).&lt;/p&gt;

&lt;h1 id=&quot;from-fast-to-elliptic&quot;&gt;From Fast to Elliptic&lt;/h1&gt;
&lt;p&gt;From Cooley and Tukey’s work sprung a great many variants of Fourier decomposition algorithms. One of them, the &lt;a href=&quot;&quot;&gt;Discrete Cosine Transformation&lt;/a&gt; was used in the MP3, JPEG and MPEG standards, being used by millions upon millions of users. A big difference with the standard Fourier series analysis is that it uses only the cosine term (\(b_n\)) as seen in &lt;a href=&quot;#MathJax-Element-1-Frame&quot;&gt;eq. 1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;From variants came other variants, amongst which the elliptical variant. The &lt;a href=&quot;https://doi.org/10.1016/0146-664X(82)90034-X&quot;&gt;Elliptic Fourier series&lt;/a&gt; was published in 1982 by Frank Kuhl and Charles Giardina. They extended the principle of time domain to a point series domain in two-dimensional space. So, rather than having N samples in time, you have a sequence of N points of a polygon in two spatial directions. The advantage here is that these N points are guaranteed to form a continuous, stable signal since together these points form a “closed contour”, as Kuhl and Giardina put it. Now, things get a little complicated. The algorithms uses four coefficients, which makes sense because we have a signal in two dimensions, so there will be two coefficients for each of the two dimensions. So for the x-coordinates we have:&lt;/p&gt;

\[a_{n}=\frac{T}{2 n^{2} \pi^{2}} \sum_{\rho=1}^{K} \frac{\Delta x_{p}}{\Delta t_{p}}\left[\cos \frac{2 n \pi t_{p}}{T}-\cos \frac{2 n \pi t_{p-1}}{T}\right]\]

\[b_{n}=\frac{T}{2 n^{2} \pi^{2}} \sum_{p=1}^{K} \frac{\Delta x_{p}}{\Delta t_{\rho}}\left[\sin \frac{2 n \pi t_{p}}{T}-\sin \frac{2 n \pi t_{p-1}}{T}\right]

\tag{9}\]

&lt;p&gt;and for the y-axis we have&lt;/p&gt;

\[c_{n}=\frac{T}{2 n^{2} \pi^{2}} \sum_{p=1}^{K} \frac{\Delta y_{p}}{\Delta t_{\rho}}\left[\cos \frac{2 n \pi t_{p}}{T}-\cos \frac{2 n \pi t_{p-1}}{T}\right]\]

\[d_{n}=\frac{T}{2 n^{2} \pi^{2}} \sum_{p=1}^{K} \frac{\Delta y_{p}}{\Delta t_{p}}\left[\sin \frac{2 n \pi t_{p}}{T}-\sin \frac{2 n \pi t_{p-1}}{T}\right]

\tag{10}\]

&lt;p&gt;This extracts the set of \({a, b, c, d}\) coefficients (&lt;a href=&quot;http://www.sci.utah.edu/~gerig/CS7960-S2010/handouts/Kuhl-Giardina-CGIP1982.pdf&quot;&gt;Kuhl &amp;amp; Giardina 1982, 239-240&lt;/a&gt;) for a series of N ellipses from the x- and y-deltas (\(\Delta x_{p}\) and \(\Delta y_{p}\)) between each consecutive point p in the K points in the polygon.&lt;/p&gt;

&lt;p&gt;Then, we also need the center-point (&lt;a href=&quot;http://www.sci.utah.edu/~gerig/CS7960-S2010/handouts/Kuhl-Giardina-CGIP1982.pdf&quot;&gt;Kuhl &amp;amp; Giardina 1982, 240&lt;/a&gt;) for the main ellipse as \(A_0\) and \(C_0\):&lt;/p&gt;

\[\begin{array}{l}{A_{0}=\frac{1}{T} \sum_{p=1}^{K} \frac{\Delta x_{p}}{2 \Delta t_{p}}\left(t_{p}^{2}-t_{p-1}^{2}\right)+\xi_{p}\left(t_{p}-t_{p-1}\right)} \\ {C_{0}=\frac{1}{T} \sum_{p=1}^{K} \frac{\Delta y_{p}}{2 \Delta t_{p}}\left(t_{p}^{2}-t_{p-1}^{2}\right)+\delta_{p}\left(t_{p}-t_{p-1}\right)}\end{array}

\tag{11}\]

&lt;p&gt;where&lt;/p&gt;

\[\begin{array}{l}{\xi_{p}=\sum_{j=1}^{p-1} \Delta x_{j}-\frac{\Delta x_{p}}{\Delta t_{p}} \sum_{j=1}^{p-1} \Delta t_{j}} \\ {\delta_{p}=\sum_{j=1}^{p-1} \Delta y_{j}-\frac{\Delta y_{p}}{\Delta t_{p}} \sum_{j=1}^{p-1} \Delta t_{j}}\end{array}

\tag{12}\]

&lt;p&gt;and \(\xi_{1}=\delta_{1}=0\)&lt;/p&gt;

&lt;p&gt;Like other Fourier series transforms, the method is invertible to create a reconstruction from the coefficients, as is shown in the demo at the top of the page.&lt;/p&gt;

&lt;p&gt;The algorithm was implemented in Matlab at least as early as 1996 by &lt;a href=&quot;https://doi.org/10.1016/0031-3203(95)00118-2&quot;&gt;Trier, Jain and Taxt&lt;/a&gt;, again in &lt;a href=&quot;https://nl.mathworks.com/matlabcentral/fileexchange/12746-elliptical-fourier-shape-descriptors&quot;&gt;2006&lt;/a&gt; and again in &lt;a href=&quot;https://nl.mathworks.com/matlabcentral/fileexchange/32800-elliptic-fourier-for-shape-analysis&quot;&gt;2016&lt;/a&gt; before it came to &lt;a href=&quot;https://github.com/hbldh/pyefd&quot;&gt;Python&lt;/a&gt; in 2016 as well. From there, it was re-implemented in &lt;a href=&quot;https://github.com/alihashmiii/Elliptical-Fourier-Descriptors&quot;&gt;Wolfram&lt;/a&gt; and I re-implemented it in &lt;a href=&quot;https://github.com/SPINLab/neighborhoods-autoencoder/blob/master/model/pytorch_efd.py&quot;&gt;Python using the Pytorch framework&lt;/a&gt;, and in &lt;a href=&quot;https://github.com/SPINLab/neighborhoods-autoencoder/blob/master/js/src/efdCoefficients.js&quot;&gt;Javascript using Tensorflow.js&lt;/a&gt; in 2019. But I find it unlikely that there was no imlementation in either Fortran or C in the fourteen years between 1982 and 1996; it simply hasn’t persisted on the web (yet).&lt;/p&gt;
</description>
        <pubDate>Fri, 12 Jul 2019 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2019/07/12/elliptical_fourier_analysis.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2019/07/12/elliptical_fourier_analysis.html</guid>
        
        
      </item>
    
      <item>
        <title>Myoware Muscle Sensor Project</title>
        <description>
</description>
        <pubDate>Wed, 08 May 2019 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2019/05/08/myoware-muscle-sensor-project.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2019/05/08/myoware-muscle-sensor-project.html</guid>
        
        
      </item>
    
      <item>
        <title>Tizen web app development using node.js, NPM and WebPack</title>
        <description>&lt;p&gt;A lot of people at my work are showing up with smartwatches. I don’t expect the smartwatch having such an impact on daily life as the smartphone did for mobile phones, it has much more the feel of a gadget. To fidget with.&lt;/p&gt;

&lt;p&gt;However, I took the step of buying one myself as well. I didn’t want to invest a lot, so I purchased a ‘second chance’ model (basically a returned item) under € 200, just to see what all the fuss is about. Are my colleagues just being silly, showing off oversized, clunky watches, or is there actually something useful to the smartwatch? I decided to find out for myself.&lt;/p&gt;

&lt;p&gt;One feature I definitely required, is that the software platform on the device must be hackable. Preferably in an open source-y kind of manner. This ruled out any stuff from any manufacturer with an ecosystem that is too self-oriented or closed. This got rid of quite a few products. I mostly compared Android and Tizen platform stuff, since these appeared to gravitate to the more open ecosystem side. I read up on some ‘versus’ posts and reviews such as &lt;a href=&quot;https://www.wareable.com/smartwatches/tizen-os-vs-android-wear&quot;&gt;this one&lt;/a&gt; and settled for a Samsung Wear S3 Frontier, also because it was very affordable.&lt;/p&gt;

&lt;h2 id=&quot;however&quot;&gt;However&lt;/h2&gt;
&lt;p&gt;To get the development environment set up and getting a first application to run, is quite, quite the hassle. Beware, for example, that Tizen Studio (the version I’m running: v3.2) does not run with the latest version of Java, which is v.11 at the moment. You need Java 8. I’m not going to run through all the motions for getting Java 8 installed and available to your system at this point, but I will suffice to say that the %JAVA_HOME% environment variable needs to be set and that the %JAVA_HOME%/bin directory of your Java install needs to be in your path. Same probably goes for Linux environments: you will need a fully-set up Java installation, which on Linux is perhaps a little easier. Trick is that when you have different Java versions installed, you can use &lt;a href=&quot;https://stackoverflow.com/questions/12787757/how-to-use-the-command-update-alternatives-config-java&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update-alternatives&lt;/code&gt;&lt;/a&gt; to select Java 8 on Linux. Prepare for a few days to get everything up and running.&lt;/p&gt;

&lt;h2 id=&quot;connecting-the-watch&quot;&gt;Connecting the watch&lt;/h2&gt;
&lt;p&gt;The S3 Frontier doesn’t have WiFi, so you connect it to Tizen Studio through a truly convoluted way: through your phone. The &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.samsung.android.app.watchmanager&quot;&gt;Samsung Wear app on Android&lt;/a&gt; is allowed to connect over bluetooth to the watch, and the btconnect sub-app somehow tunnels the watch to the pc, but only if you have developer mode enabled on your phone. Really? Really. However, in trying to connect, I did not have any success. The watch wouldn’t show up. Until I read this pivotal piece of information: &lt;a href=&quot;https://developer.samsung.com/forum/thread/unable-connect-using-sdboverbt/201/350583#post4&quot;&gt;have you tried turning it off and on again?&lt;/a&gt;. That seemed to do the trick. Unless you lose the connection, and then you need to restart your watch again.&lt;/p&gt;

&lt;p&gt;Also, I found that connecting the watch through &lt;a href=&quot;https://developer.samsung.com/galaxy-watch/design/watch-designer/&quot;&gt;Galaxy Watch Designer&lt;/a&gt; was the most stable way. You click the ‘Run on device’ button on the top right (also reached through shortcut F9) which allows you to scan for the S3 Frontier, which will take some more patience. All in all, it’s a pretty meagre development experience, but it works. That’s just getting it connected, though. You’ll have to persevere some more, if you have a non-WiFi watch. The workflow that seems to work in a repeatable fashion is like:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Connect the Android phone to your dev laptop. Make sure the watch is paired with the phone in use and that the phone is in developer mode.&lt;/li&gt;
  &lt;li&gt;Have the Samsung Wear app running on your phone&lt;/li&gt;
  &lt;li&gt;Reset the watch by powering down and up again&lt;/li&gt;
  &lt;li&gt;Open the Galaxy Watch Designer application on the dev laptop. Hit F9, which will open up a menu box.&lt;/li&gt;
  &lt;li&gt;Select the Android phone listed there by clicking on it.&lt;/li&gt;
  &lt;li&gt;This will open up the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sdboverbt&lt;/code&gt; app on your phone. Connect the watch over bluetooth by hitting the ‘SELECT DEVICE FOR DEBUGGING’ button and selecting the watch item in the list.&lt;/li&gt;
  &lt;li&gt;The Galaxy Watch Designer application will appear to do nothing. Click the phone item in the Run on Device menu again. Click on “Scan devices”.&lt;/li&gt;
  &lt;li&gt;Develop a vague sense of “this isn’t working” and select the phone item again. Click “Scan devices again”.&lt;/li&gt;
  &lt;li&gt;The Galaxy Watch Designer application seems to get stuck for a split second, then shows a popup informing you that having the watch connected over bluetooth over a prolonged period can drain the battery on the watch.&lt;/li&gt;
  &lt;li&gt;If the above steps fail to produce a working connection, reiterate from 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;You’re in!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can now add the watch device in Tizen Studio using the ‘Remote devices’ in the Device manager (Alt-Shift-V). It should be easy to find now. The cool thing is that you can&lt;/p&gt;

&lt;h2 id=&quot;permission-hurdles-certificates-features-and-priviledges&quot;&gt;Permission hurdles: certificates, features and priviledges&lt;/h2&gt;
&lt;p&gt;The second hurdle to overcome is that you need special Samsung-issued certificates to install your home-made apps on a Samsung watch. This step is a little less fickle than getting your watch connected through a phone, but it does require a few steps. The easiest method is probably through Tizen Studio. You need to install the Samsung certificate plugin through the package manager (Alt-Shift-P). Under “Extension IDE”, find the Samsung Wearable and Samsung Certificate plugins and install these. Then, follow the instructions to install the certificates. If you don’t, you will get a security error on trying to deploy your pretty app to your watch.&lt;/p&gt;

&lt;p&gt;Once you have the certificates installed, you need to declare the features and privileges your app needs. For example, if your app needs to access stuff from around the web (and who doesn’t?), you need to edit the config.xml file of your project in Tizen Studio. Add the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://tizen.org/feature/network.internet&lt;/code&gt; feature and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://tizen.org/privilege/internet&lt;/code&gt; privilege.&lt;/p&gt;

&lt;p&gt;Setting these features and privileges isn’t enough by itself, though. The user needs to grant permissions to these privileges through JavaScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tizen.ppm.requestPermission()&lt;/code&gt; calls. Fortunately through this method, the application itself can ask the user for these privileges itself, allowing the user to opt in for a default setting/’remember this choice’ that prevents the application to ask permission every time the application starts. However, the watch will ask permission each time you re-deploy a newer version of your app, though.&lt;/p&gt;

&lt;h1 id=&quot;setting-up-the-app-and-permissions&quot;&gt;Setting up the app and permissions&lt;/h1&gt;
&lt;p&gt;In order to ask the user for the required privileges, you need some code, which we will cover in some more detail. Since we are making a Tizen web app, this is done in JavaScript. The Tizen web apps a fairly recent JavaScript engine available. The command &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;navigator.userAgent&lt;/code&gt; gives the following useful information:
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Mozilla/5.0 (Linux; Tizen 4.0; SAMSUNG SM-R760) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.0 Mobile Safari/537.36&quot;&lt;/code&gt;
So although it isn’t exactly the most recent version (I have Chrome v.74 running at the moment, &lt;a href=&quot;https://developers.google.com/web/updates/2017/01/nic56&quot;&gt;56 was issued January 2017&lt;/a&gt; so over 2 years ago), it has support for ES6 language features, including &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;class&lt;/code&gt;es&lt;/a&gt;, &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Symbol&lt;/code&gt;s&lt;/a&gt; and &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions&quot;&gt;fat arrow functions&lt;/a&gt;. In a nutshell: every language feature that makes JavaScript into a great modern-age functional programming language.&lt;/p&gt;

&lt;p&gt;Not so pretty is the fact that Tizen Studio has not yet figured out the existence of ES6. It still assumes ES5 syntax. I would recommend, therefore, that at this point, we drop Tizen Studio as a development tool, and to use it just as a build tool. Instead, we are building our app in &lt;a href=&quot;https://code.visualstudio.com/Download&quot;&gt;Visual Studio Code&lt;/a&gt; and the &lt;a href=&quot;https://nodejs.org/en/download/&quot;&gt;node.js/npm&lt;/a&gt;/&lt;a href=&quot;https://www.npmjs.com/package/webpack&quot;&gt;WebPack&lt;/a&gt; toolchain.&lt;/p&gt;

&lt;p&gt;To be continued…&lt;/p&gt;
</description>
        <pubDate>Sun, 05 May 2019 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2019/05/05/tizen-web-apps-using-node-and-webpack.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2019/05/05/tizen-web-apps-using-node-and-webpack.html</guid>
        
        
      </item>
    
      <item>
        <title>Auto-starting ten Amazon machine learning instances</title>
        <description>&lt;h1 id=&quot;update&quot;&gt;UPDATE&lt;/h1&gt;
&lt;p&gt;This is part three on automating deep learning training sessions as software builds.&lt;/p&gt;

&lt;h1 id=&quot;disclaimer&quot;&gt;Disclaimer&lt;/h1&gt;
&lt;p&gt;Use any of this at your own risk. Automating machine bootup and shutdowns can be a tricky thing and I will not guarantee that the configuration below will always work.&lt;/p&gt;

&lt;h1 id=&quot;tldr&quot;&gt;TLDR;&lt;/h1&gt;
&lt;p&gt;Rent a cheap machine as a master and deploy &lt;a href=&quot;#listening&quot;&gt;this&lt;/a&gt; listening script to fire up a couple of Amazon machines to train your model.&lt;/p&gt;

&lt;p&gt;Part &lt;a href=&quot;/2017/10/25/queue-ml-builds.html&quot;&gt;one is here&lt;/a&gt;.
Part &lt;a href=&quot;/2018/04/10/deep-learning-result-spread.html&quot;&gt;two is here&lt;/a&gt;.&lt;/p&gt;

&lt;h1 id=&quot;firing-up-ten-amazon-machines&quot;&gt;Firing up ten Amazon machines&lt;/h1&gt;
&lt;p&gt;Previously, I wrote on the wonders of &lt;a href=&quot;/2017/10/25/queue-ml-builds.html&quot;&gt;using a build server to ‘build’ deep learning models&lt;/a&gt; as part of a continuous delivery pipeline of sorts. You could even go as far as publish the pre-trained and saved model as a build artifact, a ‘release’ of your exertions. We will look into that another time, but having a full build trail of a pre-trained model could be part of the answer to the &lt;a href=&quot;https://petewarden.com/2018/03/19/the-machine-learning-reproducibility-crisis/&quot;&gt;machine learning reproducibility crisis&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Then, I wrote on why it is important to &lt;a href=&quot;/2018/04/10/deep-learning-result-spread.html&quot;&gt;repeat the training results&lt;/a&gt; for a model to get a sense of the spread of the performance of your model. Many experiments in exact sciences hinge on repeated results. The Higgs boson was only confirmed after repeated measurements with a &lt;a href=&quot;https://blogs.scientificamerican.com/observations/five-sigmawhats-that/&quot;&gt;5-sigma&lt;/a&gt; or one in 3.5 million chance the hypothesis would be false.&lt;/p&gt;

&lt;p&gt;We’re not going to be that stringent, but I did go as far as to say that if you tweak your model to produce a 0.02 point increase in performance (say, accuracy) you can only claim victory if you can reproduce this gain a couple of times, because deep learning most often includes a level of randomness: in the selection of the batch and validation samples, the initialisation of the network. This is not a bad thing: we like our model to generalize as best as possible. We can’t select the data we’re going to throw at it in production either: it’s very probably going to be random.&lt;/p&gt;

&lt;p&gt;In embracing this randomness and the spread that our model performance is going to exhibit, I plead to do repeated experiments on the same model setup using (at least) ten machines. These I rent from Amazon and, as these machines are costly and not needed all of the time, I’m going to automate the crap out of it. Here is what I did.&lt;/p&gt;

&lt;h1 id=&quot;listening&quot;&gt;Listening&lt;/h1&gt;
&lt;p&gt;First, we need a machine that is always on. It is our master machine and it has the authority to fire up other machines if some change in our repository is detected. For this, we keep things as simple as possible, as simplicity is our friend. The more complexity we introduce, the more likely it is to break at some time.&lt;/p&gt;

&lt;p&gt;I used a python script to set up a service. For this script, you will need a couple of things. Firstly, it is going to read a yaml file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instances.yml&lt;/code&gt; from the same directory the script is run. It is going to look like this:&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;ec2&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;i-11111111&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;i-222222222&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;i-333333333&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;etcetera&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;It will contain the ec2 instance ids you are going to use to do your training sessions. These instance ids you can find in your Amazon EC2 console. You want to start with one machine learning instance to test the correct workings of your automation setup. Then, if everything is working to satisfaction, you can &lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/creating-an-ami-ebs.html&quot;&gt;create a private image&lt;/a&gt; of your machine and create as many ‘clones’ from the first test machine image.&lt;/p&gt;

&lt;p&gt;Secondly, the script is going to read a couple of environment variables:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;SLACK_API_TOKEN: you can generate this &lt;a href=&quot;https://api.slack.com/tokens&quot;&gt;here&lt;/a&gt;. For more information, see &lt;a href=&quot;https://get.slack.help/hc/en-us/articles/215770388-Create-and-regenerate-API-tokens&quot;&gt;here&lt;/a&gt;. Although this part is not mandatory, I cannot recommend enough the importance of some kind of notification service to follow around the startup and shutdown of your machines. If something goes wrong, you should know. Slack does this brilliantly.&lt;/li&gt;
  &lt;li&gt;SLACK_CHANNEL: the channel name that you designate for messages on starting and stopping machines. I suggest something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#monitoring&lt;/code&gt; or something. You should create the channel first.&lt;/li&gt;
  &lt;li&gt;SECRET_TOKEN: this is the canonical environment variable name as recommended by &lt;a href=&quot;https://developer.github.com/webhooks/&quot;&gt;GitHub for the webhook system&lt;/a&gt;. You need to set this webhook for the repository you want monitored. You do this by going to https://github.com/{your_username}/{repo_name}/settings/hooks and by clicking &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Add webhook&lt;/code&gt;:
&lt;img src=&quot;/images/webhook.png&quot; alt=&quot;Webhook configuration&quot; /&gt;
You generate the token using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ruby -rsecurerandom -e &apos;puts SecureRandom.hex(20)&apos;&lt;/code&gt; as suggested &lt;a href=&quot;https://developer.github.com/webhooks/securing/#setting-your-secret-token&quot;&gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;docker-compose&quot;&gt;Docker compose&lt;/h2&gt;
&lt;p&gt;I made sure these environment variables were set, by Dockerizing the service with a Docker compose configuration in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;na&quot;&gt;ml_manager&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;unless-stopped&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./:/ml_manager&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;80:4000&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;SLACK_API_TOKEN=my_slack_api_token&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;SLACK_CHANNEL=#some_channel&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;SECRET_TOKEN=secret_github_commit_hook_token&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;AWS_REGION_NAME=eu-west-1&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;AWS_ACCESS_KEY_ID=aws_secret_key_id&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;AWS_SECRET_ACCESS_KEY=aws_secret_access_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Which you can run on the listening or master server using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up -d&lt;/code&gt; Of course you need to set proper tokens for the placeholder text here. Also, you need a web server with a reverse proxy with the same route as set in your webhook configuration, pointing to your listening service, a system which I described earlier &lt;a href=&quot;/2017/10/25/queue-ml-builds.html#apache-configuration&quot;&gt;here&lt;/a&gt;. &lt;strong&gt;Make sure NOT to commit any of this in an open repository. The information will certainly be found and someone will start mining bitcoins, hacking or spambotting using YOUR account on YOUR expenses!&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;dockerfile&quot;&gt;Dockerfile&lt;/h2&gt;
&lt;p&gt;You now of course need a simple Dockerfile:&lt;/p&gt;
&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; python:latest&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; requirements.txt /requirements.txt&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip3 &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; /requirements.txt
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /ml_manager&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;python3&quot;, &quot;manager.py&quot;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Which comes with a requirements.txt file for the python dependencies:&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;boto3
slackClient
github_webhook
pyyaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-python-script&quot;&gt;The python script&lt;/h2&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;http&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;datetime&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;boto3&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;yaml&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;flask&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;github_webhook&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Webhook&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;slackclient&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SlackClient&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;SCRIPT_NAME&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__file__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;TIMESTAMP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;datetime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;:&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;.&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Get environment variables
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slack_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;SLACK_API_TOKEN&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Slack is not required
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slack_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;channel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;SLACK_CHANNEL&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# but it requires a channel if set
&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;secret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;SECRET_TOKEN&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# You really need to set a webhook secret!
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;region_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;environ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;AWS_REGION_NAME&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Initialize frameworks
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ec2_client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boto3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ec2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;region_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;region_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ec2_res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boto3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ec2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;region_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;region_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Standard Flask app
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webhook&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Webhook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;/postreceive&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;c1&quot;&gt;# Slack notification function
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slack_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SlackClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slack_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;api_call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;chat.postMessage&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Script &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot; notification: &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;start_instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;running_instances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ec2_res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;Filters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;instance-state-name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Values&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;running&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]}])&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;running_instances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ri&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;running_instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;running_instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;already running&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ec2_client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start_instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InstanceIds&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;http_status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HTTPStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ResponseMetadata&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;][&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;HTTPStatusCode&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Start instance {}: {}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http_status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SCRIPT_NAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
                   &lt;span class=&quot;s&quot;&gt;&apos;started {} with response {}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http_status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;this_instance&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ec2_res&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;Filters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;Name&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;instance-id&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;Values&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]}])][&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;notify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SCRIPT_NAME&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
                   &lt;span class=&quot;s&quot;&gt;&apos;Error starting instance {}, status {} with {}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                        &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;this_instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;state&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;


&lt;span class=&quot;c1&quot;&gt;# Read list of instances
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;instances.yml&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;instances&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;yaml&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/pushservice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;The push service is running&quot;&lt;/span&gt;


&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webhook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hook&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;push&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Defines a handler for the &apos;push&apos; event
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;on_push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;instances&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;ec2&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;start_instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;instance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;__main__&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, we can automatically fire up as much Amazon machine learning instances as we like! Of course, these will run forever until our credit card runs out. So, next, we’re going to see how we can automatically shut these instances down once they start to run idle.&lt;/p&gt;
</description>
        <pubDate>Wed, 18 Apr 2018 00:00:00 +0000</pubDate>
        <link>https://reinvantveer.github.io/2018/04/18/starting-ten-machines.html</link>
        <guid isPermaLink="true">https://reinvantveer.github.io/2018/04/18/starting-ten-machines.html</guid>
        
        
      </item>
    
  </channel>
</rss>
