~damien/blog

00b0227f5d43fa0610b8e89c99bc39fe3bea0217 — Damien Radtke 8 months ago 01dd635 master
Add public directory
65 files changed, 6590 insertions(+), 4 deletions(-)

D public/.gitignore
A public/404.html
D public/README.md
A public/apple-touch-icon-144-precomposed.png
A public/categories/index.html
A public/categories/index.xml
A public/css/hyde.css
A public/css/poole.css
A public/css/print.css
A public/css/syntax.css
A public/extras/gtk-giphy/giphy-viewer
A public/extras/gtk-giphy/giphy-viewer.sha1
A public/extras/gtk-giphy/gtk-giphy-part1.tar.gz
A public/extras/gtk-giphy/gtk-giphy-part2.tar.gz
A public/extras/gtk-giphy/gtk-giphy-part3.tar.gz
A public/extras/opensuse-vultr/autoinst.xml
A public/favicon-old.ico
A public/favicon.png
A public/images/GitHub-Mark-64px.png
A public/images/In-Black-59px-R.png
A public/images/TwitterLogo_white.png
A public/images/building-gtk-applications-like-websites/alert.png
A public/images/building-gtk-applications-like-websites/login-form-1.png
A public/images/building-gtk-applications-like-websites/login-form-2.png
A public/images/building-gtk-applications-like-websites/styling.png
A public/images/building-gtk-applications-like-websites/webby-hello.png
A public/images/building-gtk-applications-like-websites/webby.png
A public/images/gtk-giphy/giphy-homer.gif
A public/images/gtk-giphy/gtk-hello-world.png
A public/images/gtk-giphy/progress.gif
A public/images/gtk-giphy/ui-1.png
A public/images/isomorphic-golang/alert.png
A public/images/isomorphic-golang/counter.png
A public/images/isomorphic-golang/hello-go.png
A public/images/me.jpg
A public/images/overengineering-this-blog/consul-ui.png
A public/images/overengineering-this-blog/create-nodebalancer.png
A public/images/overengineering-this-blog/nomad-ui.png
A public/index.html
A public/index.xml
A public/pages/about/index.html
A public/pages/index.html
A public/pages/index.xml
A public/pages/resume/index.html
A public/post/beer-and-http-pipelines/index.html
A public/post/blog-on-nomad/index.html
A public/post/building-a-cloudfree-hashistack-cluster/index.html
A public/post/building-gtk-applications-like-websites/index.html
A public/post/cgo-api-coverage/index.html
A public/post/deploying-opensuse-on-vultr/index.html
A public/post/fleet-without-docker/index.html
A public/post/glacier-backup-bash/index.html
A public/post/go-js-template/index.html
A public/post/gopherpc/index.html
A public/post/gtk-giphy/index.html
A public/post/hot-reload-uno-gtk/index.html
A public/post/index.html
A public/post/index.xml
A public/post/nomad-task-versioning/index.html
A public/post/rusty-dynamic-loading/index.html
A public/sitemap.xml
A public/tags/index.html
A public/tags/index.xml
A public/videos/hot-reload-uno-gtk/GTK Hot Reload.mp4
A public/videos/hot-reload-uno-gtk/Uno Hot Reload.mp4
D public/.gitignore => public/.gitignore +0 -3
@@ 1,3 0,0 @@
*
!README.md
!.gitignore

A public/404.html => public/404.html +57 -0
@@ 0,0 1,57 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>404 Page not found &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <h1>404: Page not found</h1>
<p class="lead">Sorry, we've misplaced that URL or it's pointing to something that doesn't exist. <a href="https://damienradtke.com/">Head back home</a> to try finding it again.</p>
    </main>

    
  </body>
</html>

D public/README.md => public/README.md +0 -1
@@ 1,1 0,0 @@
The generated site will appear here.

A public/apple-touch-icon-144-precomposed.png => public/apple-touch-icon-144-precomposed.png +0 -0
A public/categories/index.html => public/categories/index.html +59 -0
@@ 0,0 1,59 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Categories &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  <link href="https://damienradtke.com/categories/index.xml" rel="alternate" type="application/rss+xml" title="Version 7.0" />
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <ul class="posts">

</ul>
    </main>

    
  </body>
</html>

A public/categories/index.xml => public/categories/index.xml +10 -0
@@ 0,0 1,10 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Categories on Version 7.0</title>
    <link>https://damienradtke.com/categories/</link>
    <description>Recent content in Categories on Version 7.0</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language><atom:link href="https://damienradtke.com/categories/index.xml" rel="self" type="application/rss+xml" />
  </channel>
</rss>

A public/css/hyde.css => public/css/hyde.css +250 -0
@@ 0,0 1,250 @@
/*
 *  __                  __
 * /\ \                /\ \
 * \ \ \___   __  __   \_\ \     __
 *  \ \  _ `\/\ \/\ \  /'_` \  /'__`\
 *   \ \ \ \ \ \ \_\ \/\ \_\ \/\  __/
 *    \ \_\ \_\/`____ \ \___,_\ \____\
 *     \/_/\/_/`/___/> \/__,_ /\/____/
 *                /\___/
 *                \/__/
 *
 * Designed, built, and released under MIT license by @mdo. Learn more at
 * https://github.com/poole/hyde.
 */


/*
 * Contents
 *
 * Global resets
 * Sidebar
 * Container
 * Reverse layout
 * Themes
 */


/*
 * Global resets
 *
 * Update the foundational and global aspects of the page.
 */

html {
  font-family: "PT Sans", Helvetica, Arial, sans-serif;
}
@media (min-width: 48em) {
  html {
    font-size: 16px;
  }
}
@media (min-width: 58em) {
  html {
    font-size: 20px;
  }
}


/*
 * Sidebar
 *
 * Flexible banner for housing site name, intro, and "footer" content. Starts
 * out above content in mobile and later moves to the side with wider viewports.
 */

.sidebar {
  text-align: center;
  padding: 2rem 1rem;
  color: rgba(255,255,255,.5);
  background-image: linear-gradient(#282866, #202020);
}
@media (min-width: 48em) {
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 18rem;
    text-align: left;
  }
}

/* Sidebar links */
.sidebar a {
  color: #fff;
}

/* About section */
.sidebar-about h1 {
  color: #fff;
  margin-top: 0;
  font-family: "Abril Fatface", serif;
  font-size: 3.25rem;
}

/* Sidebar nav */
.sidebar-nav {
  padding-left: 0;
  list-style: none;
}
.sidebar-nav-item {
  display: block;
}
a.sidebar-nav-item:hover,
a.sidebar-nav-item:focus {
  text-decoration: underline;
}
.sidebar-nav-item.active {
  font-weight: bold;
}

/* Sticky sidebar
 *
 * Add the `sidebar-sticky` class to the sidebar's container to affix it the
 * contents to the bottom of the sidebar in tablets and up.
 */

@media (min-width: 48em) {
  .sidebar-sticky {
    position: absolute;
    right:  1rem;
    bottom: 1rem;
    left:   1rem;
  }
}


/* Container
 *
 * Align the contents of the site above the proper threshold with some margin-fu
 * with a 25%-wide `.sidebar`.
 */

.content {
  padding-top:    4rem;
  padding-bottom: 4rem;
}

@media (min-width: 48em) {
  .content {
    max-width: 38rem;
    margin-left: 20rem;
    margin-right: 2rem;
  }
}

@media (min-width: 64em) {
  .content {
    margin-left: 22rem;
    margin-right: 4rem;
  }
}


/*
 * Reverse layout
 *
 * Flip the orientation of the page by placing the `.sidebar` on the right.
 */

@media (min-width: 48em) {
  .layout-reverse .sidebar {
    left: auto;
    right: 0;
  }
  .layout-reverse .content {
    margin-left: 2rem;
    margin-right: 20rem;
  }
}

@media (min-width: 64em) {
  .layout-reverse .content {
    margin-left: 4rem;
    margin-right: 22rem;
  }
}



/*
 * Themes
 *
 * As of v1.1, Hyde includes optional themes to color the sidebar and links
 * within blog posts. To use, add the class of your choosing to the `body`.
 */

/* Base16 (http://chriskempson.github.io/base16/#default) */

/* Red */
.theme-base-08 .sidebar {
  background-color: #ac4142;
}
.theme-base-08 .content a,
.theme-base-08 .related-posts li a:hover {
  color: #ac4142;
}

/* Orange */
.theme-base-09 .sidebar {
  background-color: #d28445;
}
.theme-base-09 .content a,
.theme-base-09 .related-posts li a:hover {
  color: #d28445;
}

/* Yellow */
.theme-base-0a .sidebar {
  background-color: #f4bf75;
}
.theme-base-0a .content a,
.theme-base-0a .related-posts li a:hover {
  color: #f4bf75;
}

/* Green */
.theme-base-0b .sidebar {
  background-color: #90a959;
}
.theme-base-0b .content a,
.theme-base-0b .related-posts li a:hover {
  color: #90a959;
}

/* Cyan */
.theme-base-0c .sidebar {
  background-color: #75b5aa;
}
.theme-base-0c .content a,
.theme-base-0c .related-posts li a:hover {
  color: #75b5aa;
}

/* Blue */
.theme-base-0d .sidebar {
  background-color: #6a9fb5;
}
.theme-base-0d .content a,
.theme-base-0d .related-posts li a:hover {
  color: #6a9fb5;
}

/* Magenta */
.theme-base-0e .sidebar {
  background-color: #aa759f;
}
.theme-base-0e .content a,
.theme-base-0e .related-posts li a:hover {
  color: #aa759f;
}

/* Brown */
.theme-base-0f .sidebar {
  background-color: #8f5536;
}
.theme-base-0f .content a,
.theme-base-0f .related-posts li a:hover {
  color: #8f5536;
}

A public/css/poole.css => public/css/poole.css +405 -0
@@ 0,0 1,405 @@
/*
 *                        ___
 *                       /\_ \
 *  _____     ___     ___\//\ \      __
 * /\ '__`\  / __`\  / __`\\ \ \   /'__`\
 * \ \ \_\ \/\ \_\ \/\ \_\ \\_\ \_/\  __/
 *  \ \ ,__/\ \____/\ \____//\____\ \____\
 *   \ \ \/  \/___/  \/___/ \/____/\/____/
 *    \ \_\
 *     \/_/
 *
 * Designed, built, and released under MIT license by @mdo. Learn more at
 * https://github.com/poole/poole.
 */


/*
 * Contents
 *
 * Body resets
 * Custom type
 * Messages
 * Container
 * Masthead
 * Posts and pages
 * Pagination
 * Reverse layout
 * Themes
 */


/*
 * Body resets
 *
 * Update the foundational and global aspects of the page.
 */

* {
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
          box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

html {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 16px;
  line-height: 1.5;
}
@media (min-width: 38em) {
  html {
    font-size: 20px;
  }
}

body {
  color: #515151;
  background-color: #fff;
  -webkit-text-size-adjust: 100%;
      -ms-text-size-adjust: 100%;
}

/* No `:visited` state is required by default (browsers will use `a`) */
a {
  color: #268bd2;
  text-decoration: none;
}
/* `:focus` is linked to `:hover` for basic accessibility */
a:hover,
a:focus {
  text-decoration: underline;
}

/* Headings */
h1, h2, h3, h4, h5, h6 {
  margin-bottom: .5rem;
  font-weight: bold;
  line-height: 1.25;
  color: #313131;
  text-rendering: optimizeLegibility;
}
h1 {
  font-size: 2rem;
}
h2 {
  margin-top: 1rem;
  font-size: 1.5rem;
}
h3 {
  margin-top: 1.5rem;
  font-size: 1.25rem;
}
h4, h5, h6 {
  margin-top: 1rem;
  font-size: 1rem;
}

/* Body text */
p {
  margin-top: 0;
  margin-bottom: 1rem;
}

strong {
  color: #303030;
}


/* Lists */
ul, ol, dl {
  margin-top: 0;
  margin-bottom: 1rem;
}

dt {
  font-weight: bold;
}
dd {
  margin-bottom: .5rem;
}

/* Misc */
hr {
  position: relative;
  margin: 1.5rem 0;
  border: 0;
  border-top: 1px solid #eee;
  border-bottom: 1px solid #fff;
}

abbr {
  font-size: 85%;
  font-weight: bold;
  color: #555;
  text-transform: uppercase;
}
abbr[title] {
  cursor: help;
  border-bottom: 1px dotted #e5e5e5;
}

/* Code */
code,
pre {
  font-family: Menlo, Monaco, "Courier New", monospace;
}
code {
  padding: .25em .5em;
  font-size: 85%;
  color: #bf616a;
  background-color: #f9f9f9;
  border-radius: 3px;
}
pre {
  display: block;
  margin-top: 0;
  margin-bottom: 1rem;
  padding: 1rem;
  font-size: .8rem;
  line-height: 1.4;
  white-space: pre;
  word-break: break-all;
  word-wrap: break-word;
  background-color: #f9f9f9;
  border-radius: 6px;
  overflow-x: scroll;
}
pre code {
  padding: 0;
  font-size: 100%;
  color: inherit;
  background-color: transparent;
}
.highlight {
  margin-bottom: 1rem;
  border-radius: 4px;
}
.highlight pre {
  margin-bottom: 0;
}

/* Quotes */
blockquote {
  padding: .5rem 1rem;
  margin: .8rem 0;
  color: #7a7a7a;
  border-left: .25rem solid #e5e5e5;
}
blockquote p:last-child {
  margin-bottom: 0;
}
@media (min-width: 30em) {
  blockquote {
    padding-right: 5rem;
    padding-left: 1.25rem;
  }
}

img {
  display: block;
  margin: 0 auto;
  max-width: 100%;
  border-radius: 5px;
  box-shadow: 2px 2px 8px 0 #202020;
}

/* Tables */
table {
  margin-bottom: 1rem;
  width: 100%;
  border: 1px solid #e5e5e5;
  border-collapse: collapse;
}
td,
th {
  padding: .25rem .5rem;
  border: 1px solid #e5e5e5;
}
tbody tr:nth-child(odd) td,
tbody tr:nth-child(odd) th {
  background-color: #f9f9f9;
}


/*
 * Custom type
 *
 * Extend paragraphs with `.lead` for larger introductory text.
 */

.lead {
  font-size: 1.25rem;
  font-weight: 300;
}


/*
 * Messages
 *
 * Show alert messages to users. You may add it to single elements like a `<p>`,
 * or to a parent if there are multiple elements to show.
 */

.message {
  margin-bottom: 1rem;
  padding: 1rem;
  color: #717171;
  background-color: #f9f9f9;
}


/*
 * Container
 *
 * Center the page content.
 */

.container {
  max-width: 38rem;
  padding-left:  1rem;
  padding-right: 1rem;
  margin-left:  auto;
  margin-right: auto;
}


/*
 * Masthead
 *
 * Super small header above the content for site name and short description.
 */

.masthead {
  padding-top:    1rem;
  padding-bottom: 1rem;
  margin-bottom: 3rem;
}
.masthead-title {
  margin-top: 0;
  margin-bottom: 0;
  color: #505050;
}
.masthead-title a {
  color: #505050;
}
.masthead-title small {
  font-size: 75%;
  font-weight: 400;
  color: #c0c0c0;
  letter-spacing: 0;
}


/*
 * Posts and pages
 *
 * Each post is wrapped in `.post` and is used on default and post layouts. Each
 * page is wrapped in `.page` and is only used on the page layout.
 */

.page,
.post {
  margin-bottom: 4em;
}

/* Blog post or page title */
.page-title,
.post-title,
.post-title a {
  color: #303030;
}
.page-title,
.post-title {
  margin-top: 0;
}

/* Meta data line below post title */
.post-date {
  display: block;
  margin-top: -.5rem;
  margin-bottom: 1rem;
  color: #9a9a9a;
}

/* Related posts */
.related {
  padding-top: 2rem;
  padding-bottom: 2rem;
  border-top: 1px solid #eee;
}
.related-posts {
  padding-left: 0;
  list-style: none;
}
.related-posts h3 {
  margin-top: 0;
}
.related-posts li small {
  font-size: 75%;
  color: #999;
}
.related-posts li a:hover {
  color: #268bd2;
  text-decoration: none;
}
.related-posts li a:hover small {
  color: inherit;
}


/*
 * Pagination
 *
 * Super lightweight (HTML-wise) blog pagination. `span`s are provide for when
 * there are no more previous or next posts to show.
 */

.pagination {
  overflow: hidden; /* clearfix */
  margin-left: -1rem;
  margin-right: -1rem;
  font-family: "PT Sans", Helvetica, Arial, sans-serif;
  color: #ccc;
  text-align: center;
}

/* Pagination items can be `span`s or `a`s */
.pagination-item {
  display: block;
  padding: 1rem;
  border: 1px solid #eee;
}
.pagination-item:first-child {
  margin-bottom: -1px;
}

/* Only provide a hover state for linked pagination items */
a.pagination-item:hover {
  background-color: #f5f5f5;
}

@media (min-width: 30em) {
  .pagination {
    margin: 3rem 0;
  }
  .pagination-item {
    float: left;
    width: 50%;
  }
  .pagination-item:first-child {
    margin-bottom: 0;
    border-top-left-radius:    4px;
    border-bottom-left-radius: 4px;
  }
  .pagination-item:last-child {
    margin-left: -1px;
    border-top-right-radius:    4px;
    border-bottom-right-radius: 4px;
  }
}

A public/css/print.css => public/css/print.css +19 -0
@@ 0,0 1,19 @@
.sidebar {
  display: none !important;
}

.content {
  margin: 0 auto;
  width: 100%;
  float: none;
  display: initial;
}

.container {
  width: 100%;
  float: none;
  display: initial;
  padding-left:  1rem;
  padding-right: 1rem;
  margin: 0 auto;
}

A public/css/syntax.css => public/css/syntax.css +66 -0
@@ 0,0 1,66 @@
.hll { background-color: #ffffcc }
 /*{ background: #f0f3f3; }*/
.c { color: #999; } /* Comment */
.err { color: #AA0000; background-color: #FFAAAA } /* Error */
.k { color: #006699; } /* Keyword */
.o { color: #555555 } /* Operator */
.cm { color: #0099FF; font-style: italic } /* Comment.Multiline */
.cp { color: #009999 } /* Comment.Preproc */
.c1 { color: #999; } /* Comment.Single */
.cs { color: #999; } /* Comment.Special */
.gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */
.ge { font-style: italic } /* Generic.Emph */
.gr { color: #FF0000 } /* Generic.Error */
.gh { color: #003300; } /* Generic.Heading */
.gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */
.go { color: #AAAAAA } /* Generic.Output */
.gp { color: #000099; } /* Generic.Prompt */
.gs { } /* Generic.Strong */
.gu { color: #003300; } /* Generic.Subheading */
.gt { color: #99CC66 } /* Generic.Traceback */
.kc { color: #006699; } /* Keyword.Constant */
.kd { color: #006699; } /* Keyword.Declaration */
.kn { color: #006699; } /* Keyword.Namespace */
.kp { color: #006699 } /* Keyword.Pseudo */
.kr { color: #006699; } /* Keyword.Reserved */
.kt { color: #007788; } /* Keyword.Type */
.m { color: #FF6600 } /* Literal.Number */
.s { color: #d44950 } /* Literal.String */
.na { color: #4f9fcf } /* Name.Attribute */
.nb { color: #336666 } /* Name.Builtin */
.nc { color: #00AA88; } /* Name.Class */
.no { color: #336600 } /* Name.Constant */
.nd { color: #9999FF } /* Name.Decorator */
.ni { color: #999999; } /* Name.Entity */
.ne { color: #CC0000; } /* Name.Exception */
.nf { color: #CC00FF } /* Name.Function */
.nl { color: #9999FF } /* Name.Label */
.nn { color: #00CCFF; } /* Name.Namespace */
.nt { color: #2f6f9f; } /* Name.Tag */
.nv { color: #003333 } /* Name.Variable */
.ow { color: #000000; } /* Operator.Word */
.w { color: #bbbbbb } /* Text.Whitespace */
.mf { color: #FF6600 } /* Literal.Number.Float */
.mh { color: #FF6600 } /* Literal.Number.Hex */
.mi { color: #FF6600 } /* Literal.Number.Integer */
.mo { color: #FF6600 } /* Literal.Number.Oct */
.sb { color: #CC3300 } /* Literal.String.Backtick */
.sc { color: #CC3300 } /* Literal.String.Char */
.sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */
.s2 { color: #CC3300 } /* Literal.String.Double */
.se { color: #CC3300; } /* Literal.String.Escape */
.sh { color: #CC3300 } /* Literal.String.Heredoc */
.si { color: #AA0000 } /* Literal.String.Interpol */
.sx { color: #CC3300 } /* Literal.String.Other */
.sr { color: #33AAAA } /* Literal.String.Regex */
.s1 { color: #CC3300 } /* Literal.String.Single */
.ss { color: #FFCC33 } /* Literal.String.Symbol */
.bp { color: #336666 } /* Name.Builtin.Pseudo */
.vc { color: #003333 } /* Name.Variable.Class */
.vg { color: #003333 } /* Name.Variable.Global */
.vi { color: #003333 } /* Name.Variable.Instance */
.il { color: #FF6600 } /* Literal.Number.Integer.Long */

.css .o,
.css .o + .nt,
.css .nt + .nt { color: #999; }

A public/extras/gtk-giphy/giphy-viewer => public/extras/gtk-giphy/giphy-viewer +0 -0
A public/extras/gtk-giphy/giphy-viewer.sha1 => public/extras/gtk-giphy/giphy-viewer.sha1 +1 -0
@@ 0,0 1,1 @@
10e85260d78fd1f807c8fdc9d6db4a8d4b9d37d7  giphy-viewer

A public/extras/gtk-giphy/gtk-giphy-part1.tar.gz => public/extras/gtk-giphy/gtk-giphy-part1.tar.gz +0 -0
A public/extras/gtk-giphy/gtk-giphy-part2.tar.gz => public/extras/gtk-giphy/gtk-giphy-part2.tar.gz +0 -0
A public/extras/gtk-giphy/gtk-giphy-part3.tar.gz => public/extras/gtk-giphy/gtk-giphy-part3.tar.gz +0 -0
A public/extras/opensuse-vultr/autoinst.xml => public/extras/opensuse-vultr/autoinst.xml +186 -0
@@ 0,0 1,186 @@
<?xml version="1.0"?>
<!DOCTYPE profile>

<profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns">
  <general>
    <mode>
      <confirm config:type="boolean">false</confirm>
    </mode>
  </general>

  <bootloader>
    <global>
      <append> resume=/dev/system/swap splash=silent quiet showopts</append>
      <append_failsafe>showopts apm=off noresume edd=off powersaved=off nohz=off highres=off processor.max_cstate=1 nomodeset x11failsafe</append_failsafe>
      <default>openSUSE Leap</default>
      <distributor>openSUSE Leap</distributor>
      <gfxbackground>/boot/grub2/themes/openSUSE/background.png</gfxbackground>
      <gfxmode>auto</gfxmode>
      <gfxtheme>/boot/grub2/themes/openSUSE/theme.txt</gfxtheme>
      <hiddenmenu>false</hiddenmenu>
      <lines_cache_id>0</lines_cache_id>
      <os_prober>true</os_prober>
      <terminal>gfxterm</terminal>
      <timeout config:type="integer">5</timeout>
    </global>

    <loader_type>grub2</loader_type>
  </bootloader>

  <!--
    Note that it's better to enable the firewall and open up only the services you need,
    but for simplicity we're just going to turn it off.
  -->
  <firewall>
    <enable_firewall config:type="boolean">false</enable_firewall>
    <start_firewall config:type="boolean">false</start_firewall>
  </firewall>

  <keyboard>
    <keymap>english-us</keymap>
  </keyboard>

  <language>
    <language>en_US</language>
    <languages>en_US</languages>
  </language>

  <networking>
    <dhcp_options>
      <dhclient_client_id/>
      <dhclient_hostname_option>AUTO</dhclient_hostname_option>
    </dhcp_options>

    <dns>
      <hostname>suse</hostname>
      <resolv_conf_policy>auto</resolv_conf_policy>
      <dhcp_hostname config:type="boolean">false</dhcp_hostname>
      <write_hostname config:type="boolean">false</write_hostname>
    </dns>

    <interfaces config:type="list">
      <interface>
        <bootproto>dhcp</bootproto>
        <device>eth0</device>
        <startmode>auto</startmode>
        <usercontrol>no</usercontrol>
      </interface>
    </interfaces>
  </networking>

  <partitioning config:type="list">
    <drive>
      <!-- Note that this is /dev/vda, not /dev/sda! -->
      <device>/dev/vda</device>
      <initialize config:type="boolean">true</initialize>
      <use>all</use>
    </drive>
  </partitioning>

  <report>
    <errors>
      <log config:type="boolean">true</log>
      <show config:type="boolean">true</show>
      <timeout config:type="integer">10</timeout>
    </errors>

    <messages>
      <log config:type="boolean">true</log>
      <show config:type="boolean">true</show>
      <timeout config:type="integer">10</timeout>
    </messages>

    <warnings>
      <log config:type="boolean">true</log>
      <show config:type="boolean">true</show>
      <timeout config:type="integer">10</timeout>
    </warnings>

    <yesno_messages>
      <log config:type="boolean">true</log>
      <show config:type="boolean">true</show>
      <timeout config:type="integer">10</timeout>
    </yesno_messages>
  </report>

  <software>
    <do_online_update config:type="boolean">false</do_online_update>
    <install_recommended config:type="boolean">true</install_recommended>

    <kernel>kernel-default</kernel>

    <patterns config:type="list">
      <pattern>base</pattern>
      <pattern>sw_management</pattern>
      <pattern>yast2_basis</pattern>
    </patterns>

    <packages config:type="list">
      <package>curl</package>
      <package>dhcp</package>
      <package>dhcp-client</package>
      <package>grub2</package>
      <package>less</package>
      <package>man</package>
      <package>sudo</package>
      <package>vim</package>
      <package>wget</package>
      <package>yast2-services-manager</package>
      <!-- Add any other packages you want installed automatically here -->
    </packages>
  </software>

  <timezone>
    <hwclock>UTC</hwclock>
    <timezone>America/Chicago</timezone>
  </timezone>

  <deploy_image>
    <image_installation config:type="boolean">false</image_installation>
  </deploy_image>

  <services-manager>
    <default_target>multi-user</default_target>

    <services>
      <enable config:type="list">
        <service>sshd</service>
      </enable>
    </services>
  </services-manager>

  <scripts>
    <chroot-scripts config:type="list">
      <script>
        <filename>newrename.sh</filename>
        <interpreter>shell</interpreter>
        <source>ln -sf /dev/null /etc/udev/rules.d/80-net-setup-link.rules</source>
        <feedback config:type="boolean">false</feedback>
        <debug config:type="boolean">true</debug>
        <chrooted config:type="boolean">true</chrooted>
      </script>

      <script>
        <filename>oldrename.sh</filename>
        <interpreter>shell</interpreter>
        <source>ln -sf /dev/null /etc/udev/rules.d/80-net-name-slot.rules</source>
        <feedback config:type="boolean">false</feedback>
        <debug config:type="boolean">true</debug>
        <chrooted config:type="boolean">true</chrooted>
      </script>
    </chroot-scripts>
  </scripts>

  <users config:type="list">
    <user>
      <username>root</username>
      <home>/root</home>
      <!-- Don't be like me, use a better password -->
      <user_password>password</user_password>
      <uid>0</uid>
      <gid>0</gid>
      <shell>/bin/bash</shell>
      <encrypted config:type="boolean">false</encrypted>
    </user>
  </users>
</profile>

A public/favicon-old.ico => public/favicon-old.ico +0 -0
A public/favicon.png => public/favicon.png +0 -0
A public/images/GitHub-Mark-64px.png => public/images/GitHub-Mark-64px.png +0 -0
A public/images/In-Black-59px-R.png => public/images/In-Black-59px-R.png +0 -0
A public/images/TwitterLogo_white.png => public/images/TwitterLogo_white.png +0 -0
A public/images/building-gtk-applications-like-websites/alert.png => public/images/building-gtk-applications-like-websites/alert.png +0 -0
A public/images/building-gtk-applications-like-websites/login-form-1.png => public/images/building-gtk-applications-like-websites/login-form-1.png +0 -0
A public/images/building-gtk-applications-like-websites/login-form-2.png => public/images/building-gtk-applications-like-websites/login-form-2.png +0 -0
A public/images/building-gtk-applications-like-websites/styling.png => public/images/building-gtk-applications-like-websites/styling.png +0 -0
A public/images/building-gtk-applications-like-websites/webby-hello.png => public/images/building-gtk-applications-like-websites/webby-hello.png +0 -0
A public/images/building-gtk-applications-like-websites/webby.png => public/images/building-gtk-applications-like-websites/webby.png +0 -0
A public/images/gtk-giphy/giphy-homer.gif => public/images/gtk-giphy/giphy-homer.gif +0 -0
A public/images/gtk-giphy/gtk-hello-world.png => public/images/gtk-giphy/gtk-hello-world.png +0 -0
A public/images/gtk-giphy/progress.gif => public/images/gtk-giphy/progress.gif +0 -0
A public/images/gtk-giphy/ui-1.png => public/images/gtk-giphy/ui-1.png +0 -0
A public/images/isomorphic-golang/alert.png => public/images/isomorphic-golang/alert.png +0 -0
A public/images/isomorphic-golang/counter.png => public/images/isomorphic-golang/counter.png +0 -0
A public/images/isomorphic-golang/hello-go.png => public/images/isomorphic-golang/hello-go.png +0 -0
A public/images/me.jpg => public/images/me.jpg +0 -0
A public/images/overengineering-this-blog/consul-ui.png => public/images/overengineering-this-blog/consul-ui.png +0 -0
A public/images/overengineering-this-blog/create-nodebalancer.png => public/images/overengineering-this-blog/create-nodebalancer.png +0 -0
A public/images/overengineering-this-blog/nomad-ui.png => public/images/overengineering-this-blog/nomad-ui.png +0 -0
A public/index.html => public/index.html +237 -0
@@ 0,0 1,237 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  <link href="https://damienradtke.com/index.xml" rel="alternate" type="application/rss+xml" title="Version 7.0" />
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    
  
		<article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/building-gtk-applications-like-websites/">Building GTK4 Applications Like Websites</a>
			</h1>
			<time datetime="2022-05-24T00:00:00Z" class="post-date">Tue, May 24, 2022</time>
			The modern web, broadly, consists of two distinct innovations:
The technology for rendering and interacting with web pages (also known as the trifecta of HTML, CSS, and JavaScript) The client-server deployment model, which allows a single desktop application (your web browser) to alter its behavior using instructions sent over the network by a web server Whether you love it or hate it, web rendering and interaction technology is widely used, both on the web and off.
			
			<div class="read-more-link">
				<a href="/post/building-gtk-applications-like-websites/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/hot-reload-uno-gtk/">Hot Reload in Uno and GTK</a>
			</h1>
			<time datetime="2021-07-30T00:00:00Z" class="post-date">Fri, Jul 30, 2021</time>
			Uno While playing around with Microsoft&rsquo;s Uno Platform, I discovered its super-neat XAML Hot Reload feature. It basically does exactly what you think it would; while the app is running, changes made to any XAML file will be reflected automatically, without needing to re-build anything. This is basically the desktop development equivalent of LiveReload, and is a great way to tighten the feedback loop and enable faster development.
Unfortunately, my laptop only runs Linux, and their documentation requires you to use the Visual Studio Add-in in order to use hot reload.
			
			<div class="read-more-link">
				<a href="/post/hot-reload-uno-gtk/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/building-a-cloudfree-hashistack-cluster/">Building a Cloud™-Free Hashistack Cluster 🌥</a>
			</h1>
			<time datetime="2020-09-12T00:00:00Z" class="post-date">Sat, Sep 12, 2020</time>
			Table of Contents Preface Getting Started Safety First: TLS Behind the Firewall Provisioning With Terraform Running a Website Final Thoughts Preface &ldquo;Hashistack&rdquo; refers to a network cluster based on HashiCorp tools, and after off-and-on spending a considerable amount of time on it, the architecture of my own cluster (on which this blog is running, among other personal projects) has finally (mostly) stabilized. In this post I will walk you through its high-level structure, some of the benefits provided, and hopefully show you how you can build a similar cluster for your personal or professional projects.
			
			<div class="read-more-link">
				<a href="/post/building-a-cloudfree-hashistack-cluster/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/blog-on-nomad/">Running this Blog on Nomad</a>
			</h1>
			<time datetime="2019-11-12T00:00:00Z" class="post-date">Tue, Nov 12, 2019</time>
			In an attempt to consolidate my various personal projects, and to be efficient about how much money I spend on VPS hosting, this blog is now running on my tiny Nomad cluster. The ID for this allocation is:
The components involved are:
Nomad Consul Vault Fabio multirootca The Nomad deployment guide recommends either three or five servers, but I&rsquo;m not really running business-critical applications, so I currently only have one server and one client node.
			
			<div class="read-more-link">
				<a href="/post/blog-on-nomad/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/gopherpc/">GopherJS and RPC over HTTP</a>
			</h1>
			<time datetime="2018-06-20T00:00:00Z" class="post-date">Wed, Jun 20, 2018</time>
			GopherJS enables web development using Go for both the backend server code and frontend browser code. One of the neat things this allows you to do, which the NodeJS community is all too happy to tell you, is share code between the frontend and backend. However, JavaScript is a dynamic language, and lacks many of the static analysis tools that Go provides. By taking advantage of Go&rsquo;s static analysis, it is possible to develop HTTP endpoints in the backend and automatically generate frontend code for calling them, using Go models and types end-to-end.
			
			<div class="read-more-link">
				<a href="/post/gopherpc/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/deploying-opensuse-on-vultr/">Deploying openSUSE on Vultr</a>
			</h1>
			<time datetime="2017-06-16T16:25:58-0700" class="post-date">Fri, Jun 16, 2017</time>
			<p>As an avid openSUSE user and fan, I wish more VPS providers supported openSUSE
images. Linode and Amazon both do, and there&rsquo;s nothing wrong with them, but I
recently learned about Vultr&rsquo;s <a href="https://www.vultr.com/features/uploadiso/">custom
ISO</a> feature and decided to try to
bring openSUSE to Vultr! Vultr provides guides for installing CoreOS and
Gentoo, after all, so why not openSUSE?</p>
			
			<div class="read-more-link">
				<a href="/post/deploying-opensuse-on-vultr/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/nomad-task-versioning/">Nomad Task Versioning</a>
			</h1>
			<time datetime="2017-03-10T15:27:58-0600" class="post-date">Fri, Mar 10, 2017</time>
			Lately I&rsquo;ve been playing around with a lot of HashiCorp tools including Nomad, their solution to application scheduling. Despite its relative immaturity, there are a few things I really like about it: straightforward, readable configuration syntax; ease of integration with other HashiCorp tools; and flexible runtime drivers, including raw execution, meaning you&rsquo;re not tied down to containers.
However, there&rsquo;s one area in which Nomad&rsquo;s documentation seems to be severely lacking: versioning.
			
			<div class="read-more-link">
				<a href="/post/nomad-task-versioning/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/fleet-without-docker/">Running Fleet Without Docker</a>
			</h1>
			<time datetime="2016-11-11T13:15:33-0600" class="post-date">Fri, Nov 11, 2016</time>
			Introduction The CoreOS project is doing some very interesting work on how to build, deploy, and scale web applications. Their big focus is to keep the platform as minimal as possible, which means that everything must be run as a container. That in turn means that the stock OS doesn&rsquo;t have to worry about any language runtimes or compilers, since those will be bundled with the app itself.
This all sounds amazing on paper, but Docker isn&rsquo;t without its flaws, and the kind of orchestration that CoreOS recommends comes with its own complexities.
			
			<div class="read-more-link">
				<a href="/post/fleet-without-docker/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/beer-and-http-pipelines/">Beer and Concurrent HTTP Pipelines</a>
			</h1>
			<time datetime="2016-10-28T13:34:04-0500" class="post-date">Fri, Oct 28, 2016</time>
			This post is going to be a variation of the famous pipelines and cancellation Go blog post, modified for crawling a website, and updated using net/http&rsquo;s native support for request contexts introduced in Go 1.7.
Downloading Beer Recipes My motivation for building a concurrent website crawler was to be able to download a catalog of all public recipes on Brewtoad. Each recipe can be individually exported as XML, but from what I could tell there was no API to be able to download them all.
			
			<div class="read-more-link">
				<a href="/post/beer-and-http-pipelines/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/rusty-dynamic-loading/">Rusty Dynamic Loading</a>
			</h1>
			<time datetime="2016-09-26T00:00:00Z" class="post-date">Mon, Sep 26, 2016</time>
			Introduction One of my favorite things that I&rsquo;ve learned so far from Casey Muratori&rsquo;s excellent Handmade Hero series of videos is his demonstration of how to load game code dynamically. This allows you to make changes to the running game without having to close the existing process, which enables very rapid iteration during development. However, Casey only shows you how to do it in C using Win32. In this post, I will demonstrate how to achieve the same basic effect using cross-platform Rust.
			
			<div class="read-more-link">
				<a href="/post/rusty-dynamic-loading/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/go-js-template/">js/template</a>
			</h1>
			<time datetime="2016-05-31T23:10:28-0500" class="post-date">Tue, May 31, 2016</time>
			<p>One of my favorite packages in the Go standard library is
<a href="https://golang.org/pkg/html/template/">html/template</a>. Not only does it provide a solid templating
language equivalent to <a href="https://golang.org/pkg/text/template/">text/template</a>, but it ensures its
safety of any HTML or JavaScript you throw at it. Unfortunately,
it can be a little limiting when working with external JavaScript resources,
but there are a couple options for working around those limitations.</p>
			
			<div class="read-more-link">
				<a href="/post/go-js-template/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/gtk-giphy/">Building a Giphy-Searching App in GTK&#43; 3</a>
			</h1>
			<time datetime="2016-04-04T00:00:00Z" class="post-date">Mon, Apr 4, 2016</time>
			<p>Web applications get all the hype these days, so why not buck the trend
and build a desktop application instead? In this post I&rsquo;m going to use
Vala and GTK+ to build a simple desktop program for searching Giphy,
the popular GIF database.</p>
			
			<div class="read-more-link">
				<a href="/post/gtk-giphy/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/glacier-backup-bash/">Amazon Glacier Backups in Bash</a>
			</h1>
			<time datetime="2016-03-24T11:46:20-0500" class="post-date">Thu, Mar 24, 2016</time>
			<p>Why use an official Java or .NET-based algorithm when you can write your own
in Bash? This script uses the AWS CLI tool to back up a file to an Amazon
Glacier vault, without the need for a heavy runtime.</p>
			
			<div class="read-more-link">
				<a href="/post/glacier-backup-bash/">Read More…</a>
			</div>
			
		</article><article class="post">
			<h1 class="post-title">
				<a href="https://damienradtke.com/post/cgo-api-coverage/">Measuring C API Coverage with Go</a>
			</h1>
			<time datetime="2013-12-03T00:00:00Z" class="post-date">Tue, Dec 3, 2013</time>
			About a year ago I started working on an initial batch of Go bindings to the Allegro 5 game library, and while I like the idea of producing a fully-functional foreign-interface library, I didn&rsquo;t finish during the initial period of development, and the project lay dormant for months. Recently I began to revive it, but as soon as I did, I ran into a problem: how much had I already done?
			
			<div class="read-more-link">
				<a href="/post/cgo-api-coverage/">Read More…</a>
			</div>
			
		</article>
  

    </main>

    
  </body>
</html>

A public/index.xml => public/index.xml +180 -0
@@ 0,0 1,180 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Version 7.0</title>
    <link>https://damienradtke.com/</link>
    <description>Recent content on Version 7.0</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 24 May 2022 00:00:00 +0000</lastBuildDate><atom:link href="https://damienradtke.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>About</title>
      <link>https://damienradtke.com/pages/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/pages/about/</guid>
      <description>Welcome to my blog!
I&amp;rsquo;m Damien, a software developer based in Chicago. This website is my place for occasionally writing blog posts, usually around programming. They&amp;rsquo;re not always interesting or useful, but hey, it&amp;rsquo;s about the journey, right?
My programming interests lately include
Go and Rust Networking infrastructure Desktop application development with GTK Because I like to think of myself as a well-rounded person, some of my non-programming interests include</description>
    </item>
    
    <item>
      <title>Resume</title>
      <link>https://damienradtke.com/pages/resume/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/pages/resume/</guid>
      <description>Information Email me@damienradtke.com Website https://damienradtke.com Phone (651) 245-3382 Location Chicago, IL Repositories github.com/dradtke — git.sr.ht/~damien Education BS in Math &amp;amp; Computer Science from DePaul University Career Experience April 2016 - present | Software Engineer @ Braintree/PayPal Current group tech lead for the Reporting Platform team Develops new features and bug fixes across a number of different systems as a senior individual contributor Primary architect of our Report Builder application, and contributing architect of our reporting data warehouse Meets with product managers to help define product requirements, provide estimates &amp;amp; insights, and answer questions Mentors and trains engineers on software development practices and internal systems Provides production support for critical reporting systems Constantly working to improve team efficiency by optimizing our tools and workflows April 2015 - April 2016 | CTO / Senior Developer @ Build This LLC Lead development of a successful Spanish language learning site Assisted with development and maintenance of web and Android apps Mentored junior developers by providing assistance and advice on best practices Met with clients to discover business requirements and collaborate on solution design February 2012 - April 2015 | Software Engineer @ Channel IQ Maintained .</description>
    </item>
    
    <item>
      <title>Building GTK4 Applications Like Websites</title>
      <link>https://damienradtke.com/post/building-gtk-applications-like-websites/</link>
      <pubDate>Tue, 24 May 2022 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/building-gtk-applications-like-websites/</guid>
      <description>The modern web, broadly, consists of two distinct innovations:
The technology for rendering and interacting with web pages (also known as the trifecta of HTML, CSS, and JavaScript) The client-server deployment model, which allows a single desktop application (your web browser) to alter its behavior using instructions sent over the network by a web server Whether you love it or hate it, web rendering and interaction technology is widely used, both on the web and off.</description>
    </item>
    
    <item>
      <title>Hot Reload in Uno and GTK</title>
      <link>https://damienradtke.com/post/hot-reload-uno-gtk/</link>
      <pubDate>Fri, 30 Jul 2021 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/hot-reload-uno-gtk/</guid>
      <description>Uno While playing around with Microsoft&amp;rsquo;s Uno Platform, I discovered its super-neat XAML Hot Reload feature. It basically does exactly what you think it would; while the app is running, changes made to any XAML file will be reflected automatically, without needing to re-build anything. This is basically the desktop development equivalent of LiveReload, and is a great way to tighten the feedback loop and enable faster development.
Unfortunately, my laptop only runs Linux, and their documentation requires you to use the Visual Studio Add-in in order to use hot reload.</description>
    </item>
    
    <item>
      <title>Building a Cloud™-Free Hashistack Cluster 🌥</title>
      <link>https://damienradtke.com/post/building-a-cloudfree-hashistack-cluster/</link>
      <pubDate>Sat, 12 Sep 2020 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/building-a-cloudfree-hashistack-cluster/</guid>
      <description>Table of Contents Preface Getting Started Safety First: TLS Behind the Firewall Provisioning With Terraform Running a Website Final Thoughts Preface &amp;ldquo;Hashistack&amp;rdquo; refers to a network cluster based on HashiCorp tools, and after off-and-on spending a considerable amount of time on it, the architecture of my own cluster (on which this blog is running, among other personal projects) has finally (mostly) stabilized. In this post I will walk you through its high-level structure, some of the benefits provided, and hopefully show you how you can build a similar cluster for your personal or professional projects.</description>
    </item>
    
    <item>
      <title>Running this Blog on Nomad</title>
      <link>https://damienradtke.com/post/blog-on-nomad/</link>
      <pubDate>Tue, 12 Nov 2019 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/blog-on-nomad/</guid>
      <description>In an attempt to consolidate my various personal projects, and to be efficient about how much money I spend on VPS hosting, this blog is now running on my tiny Nomad cluster. The ID for this allocation is:
The components involved are:
Nomad Consul Vault Fabio multirootca The Nomad deployment guide recommends either three or five servers, but I&amp;rsquo;m not really running business-critical applications, so I currently only have one server and one client node.</description>
    </item>
    
    <item>
      <title>GopherJS and RPC over HTTP</title>
      <link>https://damienradtke.com/post/gopherpc/</link>
      <pubDate>Wed, 20 Jun 2018 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/gopherpc/</guid>
      <description>GopherJS enables web development using Go for both the backend server code and frontend browser code. One of the neat things this allows you to do, which the NodeJS community is all too happy to tell you, is share code between the frontend and backend. However, JavaScript is a dynamic language, and lacks many of the static analysis tools that Go provides. By taking advantage of Go&amp;rsquo;s static analysis, it is possible to develop HTTP endpoints in the backend and automatically generate frontend code for calling them, using Go models and types end-to-end.</description>
    </item>
    
    <item>
      <title>Deploying openSUSE on Vultr</title>
      <link>https://damienradtke.com/post/deploying-opensuse-on-vultr/</link>
      <pubDate>Fri, 16 Jun 2017 16:25:58 -0700</pubDate>
      
      <guid>https://damienradtke.com/post/deploying-opensuse-on-vultr/</guid>
      <description>&lt;p&gt;As an avid openSUSE user and fan, I wish more VPS providers supported openSUSE
images. Linode and Amazon both do, and there&amp;rsquo;s nothing wrong with them, but I
recently learned about Vultr&amp;rsquo;s &lt;a href=&#34;https://www.vultr.com/features/uploadiso/&#34;&gt;custom
ISO&lt;/a&gt; feature and decided to try to
bring openSUSE to Vultr! Vultr provides guides for installing CoreOS and
Gentoo, after all, so why not openSUSE?&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>Nomad Task Versioning</title>
      <link>https://damienradtke.com/post/nomad-task-versioning/</link>
      <pubDate>Fri, 10 Mar 2017 15:27:58 -0600</pubDate>
      
      <guid>https://damienradtke.com/post/nomad-task-versioning/</guid>
      <description>Lately I&amp;rsquo;ve been playing around with a lot of HashiCorp tools including Nomad, their solution to application scheduling. Despite its relative immaturity, there are a few things I really like about it: straightforward, readable configuration syntax; ease of integration with other HashiCorp tools; and flexible runtime drivers, including raw execution, meaning you&amp;rsquo;re not tied down to containers.
However, there&amp;rsquo;s one area in which Nomad&amp;rsquo;s documentation seems to be severely lacking: versioning.</description>
    </item>
    
    <item>
      <title>Running Fleet Without Docker</title>
      <link>https://damienradtke.com/post/fleet-without-docker/</link>
      <pubDate>Fri, 11 Nov 2016 13:15:33 -0600</pubDate>
      
      <guid>https://damienradtke.com/post/fleet-without-docker/</guid>
      <description>Introduction The CoreOS project is doing some very interesting work on how to build, deploy, and scale web applications. Their big focus is to keep the platform as minimal as possible, which means that everything must be run as a container. That in turn means that the stock OS doesn&amp;rsquo;t have to worry about any language runtimes or compilers, since those will be bundled with the app itself.
This all sounds amazing on paper, but Docker isn&amp;rsquo;t without its flaws, and the kind of orchestration that CoreOS recommends comes with its own complexities.</description>
    </item>
    
    <item>
      <title>Beer and Concurrent HTTP Pipelines</title>
      <link>https://damienradtke.com/post/beer-and-http-pipelines/</link>
      <pubDate>Fri, 28 Oct 2016 13:34:04 -0500</pubDate>
      
      <guid>https://damienradtke.com/post/beer-and-http-pipelines/</guid>
      <description>This post is going to be a variation of the famous pipelines and cancellation Go blog post, modified for crawling a website, and updated using net/http&amp;rsquo;s native support for request contexts introduced in Go 1.7.
Downloading Beer Recipes My motivation for building a concurrent website crawler was to be able to download a catalog of all public recipes on Brewtoad. Each recipe can be individually exported as XML, but from what I could tell there was no API to be able to download them all.</description>
    </item>
    
    <item>
      <title>Rusty Dynamic Loading</title>
      <link>https://damienradtke.com/post/rusty-dynamic-loading/</link>
      <pubDate>Mon, 26 Sep 2016 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/rusty-dynamic-loading/</guid>
      <description>Introduction One of my favorite things that I&amp;rsquo;ve learned so far from Casey Muratori&amp;rsquo;s excellent Handmade Hero series of videos is his demonstration of how to load game code dynamically. This allows you to make changes to the running game without having to close the existing process, which enables very rapid iteration during development. However, Casey only shows you how to do it in C using Win32. In this post, I will demonstrate how to achieve the same basic effect using cross-platform Rust.</description>
    </item>
    
    <item>
      <title>js/template</title>
      <link>https://damienradtke.com/post/go-js-template/</link>
      <pubDate>Tue, 31 May 2016 23:10:28 -0500</pubDate>
      
      <guid>https://damienradtke.com/post/go-js-template/</guid>
      <description>&lt;p&gt;One of my favorite packages in the Go standard library is
&lt;a href=&#34;https://golang.org/pkg/html/template/&#34;&gt;html/template&lt;/a&gt;. Not only does it provide a solid templating
language equivalent to &lt;a href=&#34;https://golang.org/pkg/text/template/&#34;&gt;text/template&lt;/a&gt;, but it ensures its
safety of any HTML or JavaScript you throw at it. Unfortunately,
it can be a little limiting when working with external JavaScript resources,
but there are a couple options for working around those limitations.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>Building a Giphy-Searching App in GTK&#43; 3</title>
      <link>https://damienradtke.com/post/gtk-giphy/</link>
      <pubDate>Mon, 04 Apr 2016 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/gtk-giphy/</guid>
      <description>&lt;p&gt;Web applications get all the hype these days, so why not buck the trend
and build a desktop application instead? In this post I&amp;rsquo;m going to use
Vala and GTK+ to build a simple desktop program for searching Giphy,
the popular GIF database.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>Amazon Glacier Backups in Bash</title>
      <link>https://damienradtke.com/post/glacier-backup-bash/</link>
      <pubDate>Thu, 24 Mar 2016 11:46:20 -0500</pubDate>
      
      <guid>https://damienradtke.com/post/glacier-backup-bash/</guid>
      <description>&lt;p&gt;Why use an official Java or .NET-based algorithm when you can write your own
in Bash? This script uses the AWS CLI tool to back up a file to an Amazon
Glacier vault, without the need for a heavy runtime.&lt;/p&gt;</description>
    </item>
    
    <item>
      <title>Measuring C API Coverage with Go</title>
      <link>https://damienradtke.com/post/cgo-api-coverage/</link>
      <pubDate>Tue, 03 Dec 2013 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/post/cgo-api-coverage/</guid>
      <description>About a year ago I started working on an initial batch of Go bindings to the Allegro 5 game library, and while I like the idea of producing a fully-functional foreign-interface library, I didn&amp;rsquo;t finish during the initial period of development, and the project lay dormant for months. Recently I began to revive it, but as soon as I did, I ran into a problem: how much had I already done?</description>
    </item>
    
  </channel>
</rss>

A public/pages/about/index.html => public/pages/about/index.html +82 -0
@@ 0,0 1,82 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>About &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>About</h1>
    

    <p>Welcome to my blog!</p>
<p>I&rsquo;m Damien, a software developer based in Chicago. This website is my place for occasionally writing
blog posts, usually around programming. They&rsquo;re not always interesting or useful, but hey, it&rsquo;s
about the journey, right?</p>
<p>My programming interests lately include</p>
<ol>
<li>Go and Rust</li>
<li>Networking infrastructure</li>
<li>Desktop application development with GTK</li>
</ol>
<p>Because I like to think of myself as a well-rounded person, some of my non-programming interests
include</p>
<ol>
<li>Cats</li>
<li>Coffee</li>
<li>Camping</li>
<li>Board gaming</li>
</ol>
<p>The name of this blog is a reference to the opening line of Toxicity by System of a Down, and if you
got the reference already, then congratulations, you are my favorite reader. 🌟</p>

  </div>

    </main>

    
  </body>
</html>

A public/pages/index.html => public/pages/index.html +63 -0
@@ 0,0 1,63 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Pages &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  <link href="https://damienradtke.com/pages/index.xml" rel="alternate" type="application/rss+xml" title="Version 7.0" />
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <ul class="posts">
<li>
    <span><a href="https://damienradtke.com/pages/about/">About</a> <time class="pull-right post-list" datetime="0001-01-01T00:00:00Z">Mon, Jan 1, 0001</time></span>
  </li><li>
    <span><a href="https://damienradtke.com/pages/resume/">Resume</a> <time class="pull-right post-list" datetime="0001-01-01T00:00:00Z">Mon, Jan 1, 0001</time></span>
  </li>
</ul>
    </main>

    
  </body>
</html>

A public/pages/index.xml => public/pages/index.xml +31 -0
@@ 0,0 1,31 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Pages on Version 7.0</title>
    <link>https://damienradtke.com/pages/</link>
    <description>Recent content in Pages on Version 7.0</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language><atom:link href="https://damienradtke.com/pages/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>About</title>
      <link>https://damienradtke.com/pages/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/pages/about/</guid>
      <description>Welcome to my blog!
I&amp;rsquo;m Damien, a software developer based in Chicago. This website is my place for occasionally writing blog posts, usually around programming. They&amp;rsquo;re not always interesting or useful, but hey, it&amp;rsquo;s about the journey, right?
My programming interests lately include
Go and Rust Networking infrastructure Desktop application development with GTK Because I like to think of myself as a well-rounded person, some of my non-programming interests include</description>
    </item>
    
    <item>
      <title>Resume</title>
      <link>https://damienradtke.com/pages/resume/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      
      <guid>https://damienradtke.com/pages/resume/</guid>
      <description>Information Email me@damienradtke.com Website https://damienradtke.com Phone (651) 245-3382 Location Chicago, IL Repositories github.com/dradtke — git.sr.ht/~damien Education BS in Math &amp;amp; Computer Science from DePaul University Career Experience April 2016 - present | Software Engineer @ Braintree/PayPal Current group tech lead for the Reporting Platform team Develops new features and bug fixes across a number of different systems as a senior individual contributor Primary architect of our Report Builder application, and contributing architect of our reporting data warehouse Meets with product managers to help define product requirements, provide estimates &amp;amp; insights, and answer questions Mentors and trains engineers on software development practices and internal systems Provides production support for critical reporting systems Constantly working to improve team efficiency by optimizing our tools and workflows April 2015 - April 2016 | CTO / Senior Developer @ Build This LLC Lead development of a successful Spanish language learning site Assisted with development and maintenance of web and Android apps Mentored junior developers by providing assistance and advice on best practices Met with clients to discover business requirements and collaborate on solution design February 2012 - April 2015 | Software Engineer @ Channel IQ Maintained .</description>
    </item>
    
  </channel>
</rss>

A public/pages/resume/index.html => public/pages/resume/index.html +135 -0
@@ 0,0 1,135 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Resume &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>Resume</h1>
    

    <h2 id="information">Information</h2>
<!-- raw HTML omitted -->
<dl>
<dt><em>Email</em></dt>
<dd><a href="mailto:me@damienradtke.com">me@damienradtke.com</a></dd>
<dt><em>Website</em></dt>
<dd><a href="https://damienradtke.com">https://damienradtke.com</a></dd>
<dt><em>Phone</em></dt>
<dd>(651) 245-3382</dd>
<dt><em>Location</em></dt>
<dd>Chicago, IL</dd>
<dt><em>Repositories</em></dt>
<dd><a href="https://github.com/dradtke">github.com/dradtke</a> — <a href="https://git.sr.ht/~damien">git.sr.ht/~damien</a></dd>
<dt><em>Education</em></dt>
<dd>BS in Math &amp; Computer Science from DePaul University</dd>
</dl>
<!-- raw HTML omitted -->
<h2 id="career-experience">Career Experience</h2>
<dl>
<dt><em>April 2016 - present</em></dt>
<dd>| <strong>Software Engineer @ Braintree/PayPal</strong></dd>
</dl>
<ul>
<li>Current group tech lead for the Reporting Platform team</li>
<li>Develops new features and bug fixes across a number of different systems as a senior individual contributor</li>
<li>Primary architect of our Report Builder application, and contributing architect of our reporting data warehouse</li>
<li>Meets with product managers to help define product requirements, provide estimates &amp; insights, and answer questions</li>
<li>Mentors and trains engineers on software development practices and internal systems</li>
<li>Provides production support for critical reporting systems</li>
<li>Constantly working to improve team efficiency by optimizing our tools and workflows</li>
</ul>
<dl>
<dt><em>April 2015 - April 2016</em></dt>
<dd>| <strong>CTO / Senior Developer @ Build This LLC</strong></dd>
</dl>
<ul>
<li>Lead development of a successful Spanish language learning site</li>
<li>Assisted with development and maintenance of web and Android apps</li>
<li>Mentored junior developers by providing assistance and advice on best practices</li>
<li>Met with clients to discover business requirements and collaborate on solution design</li>
</ul>
<dl>
<dt><em>February 2012 - April 2015</em></dt>
<dd>| <strong>Software Engineer @ Channel IQ</strong></dd>
</dl>
<ul>
<li>Maintained .NET-based customer portal application</li>
<li>Primary maintainer of a high-profile Force.com-based web application</li>
<li>Communicated effectively across teams with account managers to resolve issues</li>
</ul>
<h2 id="skills">Skills</h2>
<p><em>Personal</em></p>
<ul>
<li>Excellent communication skills, both written and spoken</li>
<li>Proactive collaborator and team player</li>
<li>16+ years of programming experience, 10+ years professionally</li>
</ul>
<p><em>Languages and Frameworks</em></p>
<ul>
<li>Proficient with Go, Rust, Java, JavaScript, Ruby, Python, Bash, C, HTML, CSS, Markdown, Vimscript
<ul>
<li>Some experience in Scala, Lua, Haskell, C#, Groovy, Clojure, Zig, Apex, PowerShell</li>
</ul>
</li>
<li>Framework experience including Ruby on Rails, Spring Boot, Apache Airflow, and Apache Spark</li>
</ul>
<p><em>Tools and Systems</em></p>
<ul>
<li>Windows, Mac, and various Linux distributions including openSUSE, Debian, and Ubuntu</li>
<li>Shell, (Neo)Vim, Unix, Git, GitHub, JIRA, Trello, IntelliJ, Visual Studio, Jenkins</li>
<li>Microsoft Office (notably Word, Excel, Teams), G Suite, Slack, Miro, PowerPoint</li>
</ul>
<!-- raw HTML omitted -->

  </div>

    </main>

    
  </body>
</html>

A public/post/beer-and-http-pipelines/index.html => public/post/beer-and-http-pipelines/index.html +588 -0
@@ 0,0 1,588 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Beer and Concurrent HTTP Pipelines &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>Beer and Concurrent HTTP Pipelines</h1>
    
      <time datetime=2016-10-28T13:34:04-0500 class="post-date">Fri, Oct 28, 2016</time>
    

    <p>This post is going to be a variation of the famous <a href="https://blog.golang.org/pipelines">pipelines and
cancellation</a> Go blog post, modified for
crawling a website, and updated using <code>net/http</code>&rsquo;s native support for request
contexts introduced in Go 1.7.</p>
<h1 id="downloading-beer-recipes">Downloading Beer Recipes</h1>
<p>My motivation for building a concurrent website crawler was to be able to
download a catalog of all public recipes on
<a href="https://www.brewtoad.com/">Brewtoad</a>. Each recipe can be individually exported
as XML, but from what I could tell there was no API to be able to download them
all. So, I got to crawlin'.</p>
<p>(Apologies in advance to the Brewtoad team for the harm I may have done to your
server by writing this post. Internet, please be gentle. &lt;3)</p>
<h1 id="one-at-a-time">One at a Time</h1>
<p>Recipes can be viewed by visiting the URL
<code>https://www.brewtoad.com/recipes?page=X&amp;sort=rank</code>, where <code>X</code> is a page number
beginning at 1. Each page contains a series of links to specific recipes, and
the XML for that recipe can be downloaded by simply adding <code>.xml</code> to the end of
the URL. A simple first-pass, non-concurrent algorithm would then look like
this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">page</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>; ; <span style="color:#a6e22e">page</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://www.brewtoad.com/recipes?page=%d&amp;sort=rank&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">page</span>,
</span></span><span style="display:flex;"><span>    ))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// This line uses the `golang.org/x/net/html` package.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">doc</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// getRecipeLinks() implementation elided, but it walks the page,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// extracts beer recipe links, and returns them as a slice.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">recipeLink</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">getRecipeLinks</span>(<span style="color:#a6e22e">doc</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">beerXml</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;https://www.brewtoad.com&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">recipeLink</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;.xml&#34;</span>,
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Beer recipe XML acquired!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>Since the majority of time spent running this code is waiting for the network
request to finish, it makes sense that concurrently executing multiple requests
at once would speed the whole process up.</p>
<h1 id="concurrency-take-one">Concurrency: Take One</h1>
<p>The natural way to concurrify this code is to kick off a new goroutine within
each iteration of the loop:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>; ; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">page</span> <span style="color:#66d9ef">int</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;https://www.brewtoad.com/recipes?page=%d&amp;sort=rank&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">page</span>,
</span></span><span style="display:flex;"><span>        ))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">doc</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">recipeLink</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">getRecipeLinks</span>(<span style="color:#a6e22e">doc</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">recipeLink</span> <span style="color:#66d9ef">string</span>) {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">beerXml</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;https://www.brewtoad.com&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">recipeLink</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;.xml&#34;</span>,
</span></span><span style="display:flex;"><span>                )
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>                    panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Beer recipe XML acquired!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            }(<span style="color:#a6e22e">recipeLink</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }(<span style="color:#a6e22e">i</span>)
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>You could then use a channel to collect all of your downloaded recipes into a
central location.</p>
<p>There&rsquo;s one problem with this approach, though:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Get https://www.brewtoad.com/recipes?page=7136&amp;sort=rank: dial tcp 192.237.224.29:443: socket: too many open files</span></span></code></pre></div>
<p>or</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Get https://www.brewtoad.com/recipes?page=8120&amp;sort=rank: dial tcp: lookup www.brewtoad.com: no such host</span></span></code></pre></div>
<p>Exactly what error you get when you run that code may vary, but no computer
likes having thousands of HTTP requests sent off together in the blink of an
eye. While writing this post, I let that program run for a little bit too long,
and my computer&rsquo;s fan kicked into high gear. I wasn&rsquo;t able to get it back to
reasonable levels without <code>kill -9</code>.</p>
<p>This kind of &ldquo;kick off a new goroutine for each computation you need, then wait
on the results&rdquo; approach is fine for some workloads, but sending that many
concurrent HTTP requests is not one of them. So, a more nuanced approach is needed.</p>
<h1 id="concurrency-take-two">Concurrency: Take Two</h1>
<p>The natural way around this is to limit the number of goroutines that are active
at a time. This type of pattern is very common, and thanks to <code>sync.WaitGroup</code>,
very easy to implement:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">numGoroutines</span> = <span style="color:#ae81ff">50</span> <span style="color:#75715e">// can adjust based on your hardware
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wg</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">WaitGroup</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">numGoroutines</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">numGoroutines</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#a6e22e">doSomething</span>()
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Wait</span>()</span></span></code></pre></div>
<p>The next question then becomes the implementation of that <code>doSomething()</code>
function. Your goroutines will each need to download more than one page, and
they&rsquo;ll need some way of figuring out who should download which page.</p>
<p>One way to distribute the work is to &ldquo;fan-out&rdquo; by deciding which pages you
want to download, and then using a shared channel to send that input to
whichever goroutine gets it first:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">pc</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Fill the input channel with the numbers 1-100 inclusive.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">100</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">pc</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">i</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    close(<span style="color:#a6e22e">pc</span>)
</span></span><span style="display:flex;"><span>}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">numGoroutines</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Grab the next page off the channel. The `ok` value
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// will be false only if the channel has been closed,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// which means there is no more input to process.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">page</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">pc</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Download the page and collect its recipes.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Each recipe download on this page should be done
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// synchronously within this goroutine.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Done</span>()
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Wait</span>()</span></span></code></pre></div>
<p>With this change, you can now safely queue up as many pages of recipes as you&rsquo;d
like, but your computer will no longer hate you since there will be at most
<code>numGoroutines</code> HTTP requests in flight at any given point in time.</p>
<h1 id="adding-cancellation">Adding Cancellation</h1>
<p>One nice improvement is the ability to cancel all in-flight network requests.
This can be because you caught a SIGINT from the user, a certain amount of time
has passed, or any other reason. With the release of Go 1.7, this is super
easy to do since <code>http.Request</code> now supports native request-scoped contexts,
which include the ability to cancel at a moment&rsquo;s notice.</p>
<p>A simple cancellation context can be initialized like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithCancel</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>())</span></span></code></pre></div>
<p>The <code>ctx</code> context then needs to be included with each request you kick off, so
the calls to <code>http.Get()</code> then become:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewRequest</span>(<span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#a6e22e">url</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// handle err
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultClient</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))</span></span></code></pre></div>
<h1 id="the-full-example">The Full Example</h1>
<p>For those who want it, here&rsquo;s the full working code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;errors&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;fmt&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;io&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;io/ioutil&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;net/http&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;strconv&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;strings&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;sync&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;time&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;golang.org/x/net/html&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Do an initial request to determine how many pages of recipes there are
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// available to download.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">pageCount</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getRecipePageCount</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        panic(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Initialize the input (page count) channel and a cancelable request context.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">var</span> (
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">pc</span>          = make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">int</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> = <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithCancel</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>())
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Fill the input channel in a separate goroutine.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;=</span> <span style="color:#a6e22e">pageCount</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">pc</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">i</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        close(<span style="color:#a6e22e">pc</span>)
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Kick off the job. This returns two channels; one for receiving downloaded
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// recipes, and one for receiving errors.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">numGoroutines</span> = <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">rc</span>, <span style="color:#a6e22e">errc</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">downloadRecipes</span>(<span style="color:#a6e22e">numGoroutines</span>, <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">pc</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Demonstration of pipeline cancellation. After 30 seconds, every goroutine
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// will be told to drop what it&#39;s doing and exit.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">After</span>(<span style="color:#ae81ff">30</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Telling everyone to clean up.&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Wait on input values from the spawned goroutines.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// This block uses some clever channel tricks. If `ok` is false, it
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// means that the channel has been closed. A receive operation on a
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// closed channel immediately returns its type&#39;s zero value, but a
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// receive operation on a nil channel will never return. Once the
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// channel has been closed, we want to set it to nil to ensure that
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// that arm of the switch statement is never executed again.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">select</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">recipe</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">rc</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">rc</span> = <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Do something with recipe. This will probably involve
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// saving it somewhere, unmarshaling it into a struct,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// or both. Could be a good opportunity for another
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#75715e">// pipeline!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">recipe</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Got a recipe.&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">err</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">errc</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">errc</span> = <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">&#34;Error: &#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Once both channels have been closed and set to nil, we need to
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#75715e">// break out of the loop to avoid hanging indefinitely on no input.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">rc</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">errc</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// downloadRecipes spawns numGoroutines goroutines to download recipes from Brewtoad.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// The provided context can be used to cancel in-flight requests. The input channel
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// provides the page numbers that should be downloaded. This method returns two channels:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// one for downloaded recipes in XML format, and one for any errors encountered.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">downloadRecipes</span>(<span style="color:#a6e22e">numGoroutines</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">pc</span> <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">int</span>) (<span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">string</span>, <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> (
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">wg</span>   <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">WaitGroup</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">rc</span>   = make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">string</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">errc</span> = make(<span style="color:#66d9ef">chan</span> <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#a6e22e">numGoroutines</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">g</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">g</span> &lt; <span style="color:#a6e22e">numGoroutines</span>; <span style="color:#a6e22e">g</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">running</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">running</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">select</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">page</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">pc</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">running</span> = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getRecipesForPage</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">rc</span>, <span style="color:#a6e22e">page</span>)
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Don&#39;t send anything on the error channel if it was nil,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#75715e">// or if cancellation was requested, since we&#39;re trying to
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#75715e">// abort everything anyway.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Err</span>() <span style="color:#f92672">!=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Canceled</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">errc</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// A receive event on this channel means that we&#39;re cancelled,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#75715e">// so we should stop what we&#39;re doing and exit the loop.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Done</span>():
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">running</span> = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Done</span>()
</span></span><span style="display:flex;"><span>        }()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Once all goroutines have finished, close the returned channels.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Wait</span>()
</span></span><span style="display:flex;"><span>        close(<span style="color:#a6e22e">rc</span>)
</span></span><span style="display:flex;"><span>        close(<span style="color:#a6e22e">errc</span>)
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">rc</span>, <span style="color:#a6e22e">errc</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// getRecipesForPage downloads a recipe page and sends each recipe found
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// there along the provided channel.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getRecipesForPage</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">rc</span> <span style="color:#66d9ef">chan</span><span style="color:#f92672">&lt;-</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">page</span> <span style="color:#66d9ef">int</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">doc</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">downloadPage</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">page</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">link</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">findRecipeLinks</span>(<span style="color:#a6e22e">doc</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">r</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">downloadBeerXml</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">link</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">beerXml</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ioutil</span>.<span style="color:#a6e22e">ReadAll</span>(<span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">rc</span> <span style="color:#f92672">&lt;-</span> string(<span style="color:#a6e22e">beerXml</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// downloadPage downloads a recipe page from Brewtoad and parses it into
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// an HTML document.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">downloadPage</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">page</span> <span style="color:#66d9ef">int</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewRequest</span>(<span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://www.brewtoad.com/recipes?page=%d&amp;sort=rank&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">page</span>,
</span></span><span style="display:flex;"><span>    ), <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultClient</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// findRecipeLinks traverses an HTML document looking for beer recipe links.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">findRecipeLinks</span>(<span style="color:#a6e22e">doc</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>) (<span style="color:#a6e22e">links</span> []<span style="color:#66d9ef">string</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">f</span> <span style="color:#66d9ef">func</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span> = <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">n</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">FirstChild</span>; <span style="color:#a6e22e">c</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span>; <span style="color:#a6e22e">c</span> = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">NextSibling</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">f</span>(<span style="color:#a6e22e">c</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">ElementNode</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Data</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;a&#34;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">classes</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getAttr</span>(<span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Attr</span>, <span style="color:#e6db74">&#34;class&#34;</span>); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">class</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">classes</span>) {
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">class</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;recipe-link&#34;</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">href</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getAttr</span>(<span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Attr</span>, <span style="color:#e6db74">&#34;href&#34;</span>)
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">links</span> = append(<span style="color:#a6e22e">links</span>, <span style="color:#a6e22e">href</span>)
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>(<span style="color:#a6e22e">doc</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// downloadBeerXml downloads the XML for the provided recipe link. If no error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// is returned, then the io.ReadCloser must be closed by the caller in order to
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// prevent a resource leak.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">downloadBeerXml</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">link</span> <span style="color:#66d9ef">string</span>) (<span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">ReadCloser</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewRequest</span>(<span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#e6db74">&#34;https://www.brewtoad.com&#34;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">link</span><span style="color:#f92672">+</span><span style="color:#e6db74">&#34;.xml&#34;</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;failed to build beer xml request: &#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">DefaultClient</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">WithContext</span>(<span style="color:#a6e22e">ctx</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;failed to get beer xml: &#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// getRecipePageCount downloads the first page and checks the pagination to determine
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// how many pages of recipes there are.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getRecipePageCount</span>() (<span style="color:#66d9ef">int</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;https://www.brewtoad.com/recipes?page=1&amp;sort=rank&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">doc</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Parse</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">pageCount</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">f</span> <span style="color:#66d9ef">func</span>(<span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span> = <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">n</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Node</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">pageCount</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">FirstChild</span>; <span style="color:#a6e22e">c</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span>; <span style="color:#a6e22e">c</span> = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">NextSibling</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">f</span>(<span style="color:#a6e22e">c</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">ElementNode</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Data</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;a&#34;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">classes</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">getAttr</span>(<span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">Attr</span>, <span style="color:#e6db74">&#34;class&#34;</span>); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">class</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Fields</span>(<span style="color:#a6e22e">classes</span>) {
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">class</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;next_page&#34;</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">pageCount</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">strconv</span>.<span style="color:#a6e22e">Atoi</span>(<span style="color:#a6e22e">n</span>.<span style="color:#a6e22e">PrevSibling</span>.<span style="color:#a6e22e">PrevSibling</span>.<span style="color:#a6e22e">FirstChild</span>.<span style="color:#a6e22e">Data</span>)
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">f</span>(<span style="color:#a6e22e">doc</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">pageCount</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// getAttr is a utility method for looking up an HTML element attribute.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getAttr</span>(<span style="color:#a6e22e">attrs</span> []<span style="color:#a6e22e">html</span>.<span style="color:#a6e22e">Attribute</span>, <span style="color:#a6e22e">name</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">bool</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">attr</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">attrs</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">attr</span>.<span style="color:#a6e22e">Key</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">name</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">attr</span>.<span style="color:#a6e22e">Val</span>, <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>And as a bonus, a couple type definitions for unmarshaling Brewtoad recipe
results into a more usable form:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RecipeList</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Recipes</span> []<span style="color:#a6e22e">Recipe</span> <span style="color:#e6db74">`xml:&#34;RECIPE&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Recipe</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">XMLName</span>    <span style="color:#a6e22e">xml</span>.<span style="color:#a6e22e">Name</span> <span style="color:#e6db74">`xml:&#34;RECIPE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>       <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;NAME&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Type</span>       <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;TYPE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Brewer</span>     <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;BREWER&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">BatchSize</span>  <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;BATCH_SIZE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">BoilSize</span>   <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;BOIL_SIZE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">BoilTime</span>   <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;BOIL_TIME&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Efficiency</span> <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`xml:&#34;EFFICIENCY&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Style</span>      <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">StyleGuide</span>     <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;STYLE_GUIDE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Version</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;VERSION&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>           <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NAME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">StyleLetter</span>    <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;STYLE_LETTER&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CategoryNumber</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;CATEGORY_NUMBER&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Type</span>           <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;TYPE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">OGMin</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;OG_MIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">OGMax</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;OG_MAX&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">FGMin</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;FG_MIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">FGMax</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;FG_MAX&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ABVMin</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ABV_MIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ABVMax</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ABV_MAX&#34;`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#e6db74">`xml:&#34;STYLE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Fermentables</span> []<span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>           <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NAME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Origin</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ORIGIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Type</span>           <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;TYPE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Yield</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;YIELD&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Amount</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;AMOUNT&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DisplayAmount</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;DISPLAY_AMOUNT&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Potential</span>      <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;POTENTIAL&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Color</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;COLOR&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DisplayColor</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;DISPLAY_COLOR&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">AddAfterBoil</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ADD_AFTER_BOIL&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CoarseFineDiff</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;COARSE_FINE_DIFF&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Moisture</span>       <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;MOISTURE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DiastaticPower</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;DIASTATIC_POWER&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Protein</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;PROTEIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MaxInBatch</span>     <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;MAX_IN_BATCH&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">RecommendMash</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;RECOMMEND_MASH&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">IBUGalPerLB</span>    <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;IBU_GAL_PER_LB&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Notes</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NOTES&#34;`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#e6db74">`xml:&#34;FERMENTABLES&gt;FERMENTABLE&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Hops</span> []<span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NAME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Origin</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ORIGIN&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Alpha</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ALPH&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Beta</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;BETA&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Amount</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;AMOUNT&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DisplayAmount</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;DISPLAY_AMOUNT&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Use</span>           <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;USE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Form</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;FORM&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Time</span>          <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;TIME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">DisplayTime</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;DISPLAY_TIME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Notes</span>         <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NOTES&#34;`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#e6db74">`xml:&#34;HOPS&gt;HOP&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Yeasts</span> []<span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Laboratory</span>  <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;LABORATORY&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;NAME&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Type</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;TYPE&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Form</span>        <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;FORM&#34;`</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Attenuation</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`xml:&#34;ATTENUATION&#34;`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#e6db74">`xml:&#34;YEASTS&gt;YEAST&#34;`</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// TODO: The &lt;MISCS&gt; tag.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}</span></span></code></pre></div>

  </div>

    </main>

    
  </body>
</html>

A public/post/blog-on-nomad/index.html => public/post/blog-on-nomad/index.html +110 -0
@@ 0,0 1,110 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Running this Blog on Nomad &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>Running this Blog on Nomad</h1>
    
      <time datetime=2019-11-12T00:00:00Z class="post-date">Tue, Nov 12, 2019</time>
    

    <p>In an attempt to consolidate my various personal projects, and to be efficient
about how much money I spend on VPS hosting, this blog is now running on my tiny
Nomad cluster. The ID for this allocation is:</p>
<p><!-- raw HTML omitted -->
<!-- raw HTML omitted --></p>
<p>The components involved are:</p>
<ol>
<li><a href="https://www.nomadproject.io/">Nomad</a></li>
<li><a href="https://www.consul.io/">Consul</a></li>
<li><a href="https://www.vaultproject.io/">Vault</a></li>
<li><a href="https://fabiolb.net/">Fabio</a></li>
<li><a href="https://github.com/cloudflare/cfssl#the-multirootca">multirootca</a></li>
</ol>
<p>The Nomad deployment guide recommends either <a href="https://www.nomadproject.io/docs/internals/consensus.html#deployment-table">three or
five</a>
servers, but I&rsquo;m not really running business-critical applications, so I
currently only have one server and one client node.</p>
<p>The <code>server</code> node is running one instance each of Consul, Nomad, and Vault, the
first two in server mode, with certificate authorities defined on a central
<code>support</code> server.</p>
<p>I save all of the config files in my
<a href="https://git.sr.ht/~damien/infrastructure">infrastructure</a> repo. In particular,
these job files are responsible for running this blog:</p>
<ol>
<li><a href="https://git.sr.ht/~damien/infrastructure/tree/master/jobs/damienradtkecom.nomad">damienradtkecom.nomad</a> (Hugo server)</li>
<li><a href="https://git.sr.ht/~damien/infrastructure/tree/master/jobs/fabio.nomad">fabio.nomad</a> (load balancer)</li>
<li><a href="https://git.sr.ht/~damien/infrastructure/tree/master/jobs/acme-renewer.nomad">acme-renewer.nomad</a> (certificate renewer periodic batch job)</li>
</ol>
<h3 id="damienradtkecom">damienradtkecom</h3>
<p>This job is responsible for running <code>hugo server</code> on the blog&rsquo;s source
directory. It specifies the service tag expected by Fabio so that requests to
<code>damienradtke.com</code> get routed to the blog server.</p>
<p>It also runs two instances and specifies an <code>update</code> block to ensure
zero-downtime deployments.</p>
<h3 id="fabio">fabio</h3>
<p>This job runs the Fabio load balancer on a randomly-assigned port so that it
doesn&rsquo;t require root privileges, along with a custom, tiny Go program running as
root that routes traffic from port 443 to Fabio.</p>
<p>One upside to having only one client node is that the domain&rsquo;s A record can be
set to the client node&rsquo;s IP address, so traffic will properly make its way to
fabio. In case of a multi-client cluster, one node will need to be designated
the load balancer node, and the fabio job configured to always run on it.</p>
<h3 id="acme-renewer">acme-renewer</h3>
<p>This is a periodic batch job that uses <code>acme.sh</code> to renew the domain&rsquo;s SSL
certificate using a DNS challenge and the Linode API. The results are stored in
Vault&rsquo;s KV store, which Fabio is configured to read from to support HTTPS.</p>

  </div>

    </main>

    
  </body>
</html>

A public/post/building-a-cloudfree-hashistack-cluster/index.html => public/post/building-a-cloudfree-hashistack-cluster/index.html +497 -0
@@ 0,0 1,497 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Building a Cloud™-Free Hashistack Cluster 🌥 &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>Building a Cloud™-Free Hashistack Cluster 🌥</h1>
    
      <time datetime=2020-09-12T00:00:00Z class="post-date">Sat, Sep 12, 2020</time>
    

    <h2 id="table-of-contents">Table of Contents</h2>
<ol>
<li><a href="#preface">Preface</a></li>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#safety-first-tls">Safety First: TLS</a></li>
<li><a href="#behind-the-firewall">Behind the Firewall</a></li>
<li><a href="#provisioning-with-terraform">Provisioning With Terraform</a></li>
<li><a href="#running-a-website">Running a Website</a></li>
<li><a href="#final-thoughts">Final Thoughts</a></li>
</ol>
<h2 id="preface">Preface</h2>
<p>&ldquo;Hashistack&rdquo; refers to a network cluster based on <a href="https://www.hashicorp.com/">HashiCorp</a> tools, and
after off-and-on spending a considerable amount of time on it, the architecture of my own cluster
(on which this blog is running, among other personal projects) has finally (mostly) stabilized. In
this post I will walk you through its high-level structure, some of the benefits provided, and
hopefully show you how you can build a similar cluster for your personal or professional projects.</p>
<p>Everything I discuss here is available publicly in my <a href="https://git.sr.ht/~damien/infrastructure/">infrastructure
repo</a>. I reference it frequently, and some may find it
helpful simply to browse through that code.</p>
<h2 id="the-cloud">The Cloud™</h2>
<p>The term &ldquo;cloud&rdquo; is not very well-defined, but usually refers to those platforms that provide
managed services beyond Virtual Private Server (VPS) provisioning. In this post, I want to draw a
distinction between VPS providers and Cloud™ providers. While there are many different VPS
providers, here I use the term Cloud™ to refer to the big 3: Amazon Web Services, Microsoft Azure,
and Google Cloud Platform.</p>
<p>There is nothing inherently wrong with building applications on the Cloud™, but it&rsquo;s an area that is
already heavily discussed and supported, especially with the recent launch of <a href="https://www.hashicorp.com/cloud-platform/">HashiCorp Cloud
Platform</a>. The benefit of these services is that they let
you get up and running quickly, but they also often come with a hefty price tag, and can make it
harder to switch providers in the future for cost or other reasons. Using VPS providers can save you
money, makes it easier to switch and, in my opinion, is more fun.</p>
<p>My cluster is built on <a href="https://www.linode.com/">Linode</a> because they offer openSUSE VPS images and
DNS management. However, this guide should still be relevant no matter what distribution you&rsquo;re
using, though with some extra steps if you do not have
<a href="https://www.freedesktop.org/wiki/Software/systemd/"><code>systemd</code></a> or
<a href="https://firewalld.org/"><code>firewalld</code></a> available.</p>
<h2 id="getting-started">Getting Started</h2>
<h3 id="create-a-support-server">Create a Support Server</h3>
<p>A primary fixture in my setup is the use of a &ldquo;support&rdquo; server, which is a single VPS instance that
acts as the entrypoint for the rest of the cluster. Most of the infrastructure is provisioned with
Terraform and is designed to be easily replaceable; the support server is the lone instance which is
cared for as a <a href="http://cloudscaling.com/blog/cloud-computing/the-history-of-pets-vs-cattle/">pet</a>
rather than cattle. This is very similar in concept to a bastion server, but with less of a focus on
security, and more on cost savings and functionality.</p>
<p>The support server&rsquo;s functions include:</p>
<ol>
<li>Cluster members are provisioned with a random root password which is never exposed; <strong>access is
only granted via SSH public keys</strong>, and never to <code>root</code> (after provisioning has finished).
Restricting authorized keys to only what is available on the support server is an easy way to
tighten your security. (My setup is actually slightly different in that servers only allow access
with the public keys defined in Linode and I always forward my SSH agent to the support server,
but I still do all cluster operations on the support server.)</li>
<li>The support server acts as the <strong>guardian of the Certificate Authorities</strong>, and new certificates
are only issued by making a request to the support server.</li>
<li>The support server <strong>maintains Terraform state</strong>. Setting up a backend is an option here as well,
but for relatively simple uses like mine, it&rsquo;s easier to stick with the &ldquo;local&rdquo; backend on the
support server.</li>
<li><strong>Cheap artifact hosting</strong>. As long as you have a server running with a known address, you can have
your support server host all your artifacts and serve them with <a href="https://min.io/">minio</a> or even
a plain HTTP server.</li>
</ol>
<h3 id="a-note-on-ipv6">A Note on IPv6</h3>
<p>Where possible, everything is configured to communicate over IPv6. Despite its slow adoption, IPv6
is a good choice here because it is more efficient, opens up another possible route for cost savings
due to the scarcity of IPv4 addresses, and VPS providers are more likely to support it than Internet
Service Providers anyway.</p>
<h2 id="safety-first-tls">Safety First: TLS</h2>
<p>In order to safely restrict access to cluster resources, the first step you&rsquo;ll want to take with
your support server is to generate Certificate Authorities that can be used to configure TLS for
each of the services. My setup largely follows the approach outlined in HashiCorp&rsquo;s guide to
<a href="https://learn.hashicorp.com/nomad/transport-security/enable-tls">enabling TLS for Nomad</a>, which
will go more in-depth in how to use <code>cfssl</code> to get set up.</p>
<p>It might be overkill, but I use a different CA for each service, and they are stored on the support
server under <code>/etc/ssl</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/etc/ssl
</span></span><span style="display:flex;"><span>├── consul
</span></span><span style="display:flex;"><span>│   ├── ca-key.pem
</span></span><span style="display:flex;"><span>│   └── ca.pem
</span></span><span style="display:flex;"><span>├── nomad
</span></span><span style="display:flex;"><span>│   ├── ca-key.pem
</span></span><span style="display:flex;"><span>│   └── ca.pem
</span></span><span style="display:flex;"><span>└── vault
</span></span><span style="display:flex;"><span>    ├── ca-key.pem
</span></span><span style="display:flex;"><span>    └── ca.pem</span></span></code></pre></div>
<p>Another important security note is that the key permissions should be as restrictive as possible:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>-r-------- 1 root root  227 Jul 23  2019 /etc/ssl/consul/ca-key.pem
</span></span><span style="display:flex;"><span>-r--r--r-- 1 root root 1249 Jul 23  2019 /etc/ssl/consul/ca.pem</span></span></code></pre></div>
<h3 id="cfssl-configuration">CFSSL Configuration</h3>
<p>CFSSL is a general-purpose CLI tool for managing TLS files, but it also has the
ability to run a server process for handling new certificate requests. That
requires defining a configuration file at <code>/etc/ssl/cfssl.json</code> on the support server:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;signing&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;default&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;expiry&#34;</span>: <span style="color:#e6db74">&#34;87600h&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;usages&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;signing&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;key encipherment&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;server auth&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;client auth&#34;</span>
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;auth_key&#34;</span>: <span style="color:#e6db74">&#34;primary&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;auth_keys&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;primary&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;standard&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;key&#34;</span>: <span style="color:#e6db74">&#34;...&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>The <code>primary</code> auth key here must be a 16-bit hex value, and is used to prevent unauthorized parties
from requesting certificates. All new certificate requests effectively use that key as a password,
so treat it just like you would treat your private keys by <em>never</em> checking it into source control.
For more details on CFSSL configuration, see CloudFlare&rsquo;s post on <a href="https://blog.cloudflare.com/how-to-build-your-own-public-key-infrastructure/">building your own public key
infrastructure</a>.</p>
<p>There is one other tool that CFSSL provides but isn&rsquo;t mentioned in the article called <code>multirootca</code>,
which is effectively just a multiplexer for CFSSL. By default, the CFSSL server will only issue
certificates for a single Certificate Authority; <code>multirootca</code> lets you run the server in a way that
supports multiple authorities. It requires its own configuration file, but a very simple one:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-conf" data-lang="conf"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">[</span> consul <span style="color:#960050;background-color:#1e0010">]</span>
</span></span><span style="display:flex;"><span>private <span style="color:#f92672">=</span> file<span style="color:#960050;background-color:#1e0010">:///</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>consul<span style="color:#960050;background-color:#1e0010">/</span>ca-key.pem
</span></span><span style="display:flex;"><span>certificate <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>consul<span style="color:#960050;background-color:#1e0010">/</span>ca.pem
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>cfssl.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">[</span> vault <span style="color:#960050;background-color:#1e0010">]</span>
</span></span><span style="display:flex;"><span>private <span style="color:#f92672">=</span> file<span style="color:#960050;background-color:#1e0010">:///</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>vault<span style="color:#960050;background-color:#1e0010">/</span>ca-key.pem
</span></span><span style="display:flex;"><span>certificate <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>vault<span style="color:#960050;background-color:#1e0010">/</span>ca.pem
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>cfssl.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">[</span> nomad <span style="color:#960050;background-color:#1e0010">]</span>
</span></span><span style="display:flex;"><span>private <span style="color:#f92672">=</span> file<span style="color:#960050;background-color:#1e0010">:///</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>nomad<span style="color:#960050;background-color:#1e0010">/</span>ca-key.pem
</span></span><span style="display:flex;"><span>certificate <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>nomad<span style="color:#960050;background-color:#1e0010">/</span>ca.pem
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> <span style="color:#960050;background-color:#1e0010">/</span>etc<span style="color:#960050;background-color:#1e0010">/</span>ssl<span style="color:#960050;background-color:#1e0010">/</span>cfssl.json</span></span></code></pre></div>
<p>The <code>multirootca</code>
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/services/support/multirootca.service">service</a>
is then run under systemd so that it can keep running in the background, serving incoming
certificate requests.</p>
<p>Issuing new certificates is done from every cluster member via <a href="https://git.sr.ht/~damien/infrastructure/tree/master/scripts/issue-cert.sh">this
script</a>, which uses the
CFSSL CLI to make a <code>gencert</code> request to the running <code>multirootca</code> service on the support server.
Like the support server, certs and keys on cluster members all live under <code>/etc/ssl</code>, grouped by
application name, including the public key for the certificate authority.</p>
<p>One thing to note is how Consul, Nomad, and Vault interact with each other, since that affects which
certificates you need to issue. Vault depends on Consul, and Nomad depends on both Consul and Vault,
so an instance running a Nomad agent will have a lot of certificates in <code>/etc/ssl/nomad</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>-rw-r--r-- 1 nomad nomad 692 Jun 14 21:59 ca.pem
</span></span><span style="display:flex;"><span>-r--r----- 1 nomad nomad 228 Jun 14 22:00 cli-key.pem
</span></span><span style="display:flex;"><span>-rw-r--r-- 1 nomad nomad 714 Jun 14 22:00 cli.pem
</span></span><span style="display:flex;"><span>-r-------- 1 nomad nomad 228 Jun 14 22:00 consul-key.pem
</span></span><span style="display:flex;"><span>-rw-r--r-- 1 nomad nomad 970 Jun 14 22:00 consul.pem
</span></span><span style="display:flex;"><span>-r-------- 1 nomad nomad 228 Jun 15 19:07 nomad-key.pem
</span></span><span style="display:flex;"><span>-rw-r--r-- 1 nomad nomad 803 Jun 15 19:07 nomad.pem
</span></span><span style="display:flex;"><span>-r-------- 1 nomad nomad 228 Jun 14 22:00 vault-key.pem
</span></span><span style="display:flex;"><span>-rw-r--r-- 1 nomad nomad 714 Jun 14 22:00 vault.pem</span></span></code></pre></div>
<h3 id="a-note-on-hostnames">A Note on Hostnames</h3>
<p>While working on this project, the most common TLS-related errors I encountered were &ldquo;unknown
certificate authority&rdquo; and &ldquo;bad hostname.&rdquo; The former is usually pretty easy to fix; just ensure
<code>ca.pem</code> is available on every node and that it&rsquo;s being used as the relevant CA in the configs; but
the latter requires a little more thought.</p>
<p>Every node needs to consider how it is going to be queried. By default, <code>issue-cert.sh</code> considers
only <code>localhost</code> to be a valid hostname, which means that only API requests to <code>localhost</code> will be
accepted, which in turn means that all requests from another location (like the support server) will
be rejected. If you want to query your node using another name, it needs to be included as a valid
hostname when the certificate is issued.</p>
<p>For all nodes, the public IP address is a common alternative hostname to specify. This will let you
query the node from anywhere as long as your CLI is configured with its own valid certificate (a
separate <a href="https://git.sr.ht/~damien/infrastructure/tree/master/tools/issue-cert">script</a> makes this
pretty easy; it&rsquo;s very similar to the one used during node provisioning, but it operates directly on
the CA private key instead of using the remote).</p>
<p>In addition, there are a couple special cases to consider:</p>
<ol>
<li>Consul services should add <code>&lt;name&gt;.service.consul</code> as a valid hostname. Both Nomad and Vault
servers register their own services, so they should add <code>nomad.service.consul</code> and
<code>vault.service.consul</code> respectively.</li>
<li>All Nomad agents, both servers and clients, should add their <a href="https://learn.hashicorp.com/nomad/transport-security/enable-tls#node-certificates">special
hostname</a>,
which is constructed from the agent&rsquo;s role and region. All Nomad agents in my cluster stick with
the default region <code>global</code>, so Nomad servers use <code>server.global.nomad</code> and clients use
<code>client.global.nomad</code>.</li>
</ol>
<h2 id="behind-the-firewall">Behind the Firewall</h2>
<p>With any cluster, a properly-configured firewall is a <em>must</em>. I use
<a href="https://firewalld.org/"><code>firewalld</code></a>, which is the new default for openSUSE, and it&rsquo;s not too
difficult to configure.</p>
<p><code>firewalld</code> defines two important concepts for classifying incoming connections: <strong>services</strong> and
<strong>zones</strong>. Services simply define a list of protocol/port pairs that are identified by a name; for
example, the <code>ssh</code> service would be defined as <code>tcp/22</code>, because it requires TCP connections on port
22. Zones, roughly speaking, are used to classify where a connection is coming from, and what should
be done with it, such as &ldquo;for any connection to one of these services, from one of these IP
addresses, accept it.&rdquo; Connections that aren&rsquo;t explicitly given access will be dropped by default.</p>
<p>The full list of features <code>firewalld</code> provides for zones is outside the scope of this post, and if
you plan to use <code>firewalld</code>, it&rsquo;s probably good to <a href="https://www.linuxjournal.com/content/understanding-firewalld-multi-zone-configurations">read
more</a>.
However, it is still useful even with a very simple configuration.</p>
<p>One benefit of having TLS configured for Consul, Nomad, and Vault is that it is perfectly safe to
open their ports to any incoming connection regardless of source IP, since connections will be
rejected if they do not have a valid client certificate anyway.  There is a lot of room for
flexibility here though, and further restrictions may be wanted if you expect <a href="https://www.youtube.com/watch?v=xpfCr4By71U">sensitive
information</a> to go through your cluster.</p>
<h3 id="creating-a-cluster-only-zone">Creating a Cluster-Only Zone</h3>
<p>The natural fit for a more secure zone is one that only processes requests coming from other nodes
inside your cluster. While my setup leaves many ports open to the world, there is one exception:
Nomad client dynamic ports. While connections to Nomad directly require a client certificate, I
wanted my applications running on Nomad to be able to communicate with each other (more on that
below), and that requires opening up the dynamic port range to the other Nomad clients.</p>
<p>To do this, I created a new service called
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/firewall/services/nomad-dynamic-ports.xml"><code>nomad-dynamic-ports</code></a>
that grants access to the <a href="https://www.nomadproject.io/docs/job-specification/network#dynamic-ports">port
range</a> used by Nomad. All
applications running on Nomad that request a port will be assigned a random one from this range, so
we want to open up the whole range, but <em>only to other Nomad clients</em>.</p>
<p>Each Nomad client is provisioned with a zone called <code>nomad-clients</code>, which allows access to the
<code>nomad-dynamic-ports</code> service, but with no other information, so by default no connections will land
in this zone. In order for it to work, we need to add the IP address of every other Nomad client as
a source to this zone, and to do this for all the clients.</p>
<p>To do this, I wrote a
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/tools/update-nomad-client-firewall">script</a>
that uses Terraform output to get a list of all the Nomad client IP addresses, then SSH on to each
one and make the necessary updates. This script can be run automatically by Terraform with a
<a href="https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource"><code>null_resource</code></a>,
which will help keep things in sync.</p>
<h2 id="provisioning-with-terraform">Provisioning With Terraform</h2>
<p>Terraform was not actually my go-to solution for provisioning. Initially my plan was to stay simple
and stick with scripts like <code>deploy-new-server.sh</code> using Linode&rsquo;s API, but I ended up moving over to
Terraform for one big reason: state management. Terraform&rsquo;s big win is keeping track of what you&rsquo;ve
already deployed, which makes cluster management much easier. In particular, you can provision your
client nodes with <a href="https://git.sr.ht/~damien/infrastructure/tree/c97e0e2b/terraform/nomad-client/main.tf#L70-74">existing
knowledge</a>
of your Consul servers, and write
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/tools/update-nomad-client-firewall">scripts</a>
that can use that knowledge after-the-fact to make additional changes. All of these operations are
much easier with a state management tool than they would be if you had to query your VPS&rsquo; API every
time you wanted to know a node&rsquo;s IP address.</p>
<h3 id="overall-structure">Overall Structure</h3>
<p><a href="https://duckduckgo.com/?q=how+to+organize+terraform+files">How to organize Terraform code</a> is a
question of constant debate, and the right answer is that there is no right answer. A lot of it
depends on how you organize your teams, so bear in mind that my cluster is maintained by a team of
one.</p>
<p>My module structure has one top-level module, with one module for each &ldquo;role&rdquo; that my nodes will
play:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>├── main.tf
</span></span><span style="display:flex;"><span>├── outputs.tf
</span></span><span style="display:flex;"><span>├── secrets.tfvars
</span></span><span style="display:flex;"><span>├── consul-server
</span></span><span style="display:flex;"><span>│   ├── main.tf
</span></span><span style="display:flex;"><span>│   ├── outputs.tf
</span></span><span style="display:flex;"><span>│   └── variables.tf
</span></span><span style="display:flex;"><span>├── nomad-client
</span></span><span style="display:flex;"><span>│   ├── main.tf
</span></span><span style="display:flex;"><span>│   ├── outputs.tf
</span></span><span style="display:flex;"><span>│   └── variables.tf
</span></span><span style="display:flex;"><span>├── nomad-server
</span></span><span style="display:flex;"><span>│   ├── main.tf
</span></span><span style="display:flex;"><span>│   ├── outputs.tf
</span></span><span style="display:flex;"><span>│   └── variables.tf
</span></span><span style="display:flex;"><span>└── vault-server
</span></span><span style="display:flex;"><span>    ├── main.tf
</span></span><span style="display:flex;"><span>    ├── outputs.tf
</span></span><span style="display:flex;"><span>    └── variables.tf</span></span></code></pre></div>
<p>Each of these modules has a number of variables in common, including how many instances to create,
which image to use when creating the node, and a couple of other values. Most of their inputs are
the same, but this provides a lot of flexibility, and common values are usually sourced from a block
of shared <code>locals</code>.</p>
<p>This setup has several advantages, primarily flexibility and a <a href="https://git.sr.ht/~damien/infrastructure/tree/master/terraform/main.tf">top-level
<code>main.tf</code></a> that is able to
describe the makeup of your cluster very cleanly, but the downside is that it is fairly verbose
within the module definitions. Terraform doesn&rsquo;t appear to provide any utilities for defining a
set of provisioners that can be shared across resources, which would help quite a bit.</p>
<h3 id="division-of-provision">Division of Provision</h3>
<p>The provisioning of a new node is split between a custom
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/stackscripts/cluster-member.sh">stackscript</a>
and Terraform <a href="https://www.terraform.io/docs/provisioners/index.html">provisioners</a>. The stackscript
installs packages and does other configuration that is common across nodes, while the Terraform
provisioners are used to copy <a href="https://git.sr.ht/~damien/infrastructure/tree/master/config">configuration
files</a> up from the infra repo directly
and to write configuration files that are dependent on cluster knowledge, such as the addresses of
the Consul servers.</p>
<p>An alternative, and arguably better, setup would be to use <a href="https://www.packer.io/">Packer</a> to
define the node images, leaving nothing for Terraform to do except deploy instances and do the
little configuration that requires cluster knowledge. Unfortunately, this is an area where Linode
may not be a great choice; while Packer does support a Linode image builder, custom Linode images
don&rsquo;t appear to be compatible with their network helper tool, which causes basic networking to be
<a href="https://git.sr.ht/~damien/infrastructure/tree/c97e0e2b/packer/README.md">broken by default</a>.</p>
<h3 id="naming-things">Naming Things</h3>
<p>Initially, I took a very simple approach to naming nodes by their role, region, and an index, such
as <code>nomad-server-ca-central-1</code>. However, this approach lacks flexibility when it comes to upgrading
your cluster. If you want to replace a node, it is safest to create a new one and make sure it&rsquo;s up
and running before destroying the old one, but now your carefully numbered servers are no longer in
order.</p>
<p>Fortunately, Terraform provides a <a href="https://registry.terraform.io/providers/hashicorp/random/latest/docs">random
provider</a> that can be used to
name your nodes instead by generating random identifiers. I use something similar to this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;random_id&#34; &#34;servers&#34;</span> {
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">servers</span>
</span></span><span style="display:flex;"><span>    keepers <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        datacenter     <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">datacenter</span>
</span></span><span style="display:flex;"><span>        image          <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">image</span>
</span></span><span style="display:flex;"><span>        instance_type  <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">instance_type</span>
</span></span><span style="display:flex;"><span>        consul_version <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">consul_version</span>
</span></span><span style="display:flex;"><span>        nomad_version  <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">nomad_version</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    byte_length <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;linode_instance&#34; &#34;servers&#34;</span> {
</span></span><span style="display:flex;"><span>    count <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">servers</span>
</span></span><span style="display:flex;"><span>    label <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nomad-server-${var.datacenter}-${replace(random_id.servers[count.index].b64_url, &#34;-&#34;, &#34;_&#34;)}&#34;</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>This gives each Nomad server a name like <code>nomad-server-ca-central-XXXXXX</code>, where <code>XXXXXX</code> is a
base64-encoded random string. The URL-safe base64-encoding is used, but Linode doesn&rsquo;t allow two
consecutive dashes in instance labels, so the <code>replace()</code> function is used to replace dashes with
underscores in order to prevent a provision failure caused by a dash as the first letter in a server
id. (It&rsquo;s happened to me once already, not a fun reason for the apply to fail)</p>
<h2 id="running-a-website">Running a Website</h2>
<p>At this point, we&rsquo;ve covered pretty much everything you need to be able to spin up a functional
cluster.  However, as I mentioned before, this blog is currently running on my own cluster, and
there are a number of extra steps that need to be taken in order to support running a website. In
this section, I will cover the points that are specific to running a website on this setup. While
web servers are similar to any other job type in many respects, there are a few additional concerns
that bear special mention.</p>
<h3 id="load-balancing">Load Balancing</h3>
<p>Running a website on Nomad makes it easy to scale up, but running more than one instance of a web
server requires some form of load balancer. The big name in load balancers is
<a href="http://www.haproxy.org/">HAProxy</a>, but a few newer ones can take advantage of Consul&rsquo;s
service-registration features in order to &ldquo;just work&rdquo; with no or minimal configuration. For this
website I chose <a href="https://fabiolb.net/">Fabio</a>, but <a href="https://docs.traefik.io/">Traefik</a> is another
good option.</p>
<p>Regardless of which you choose, you will then have to decide how to run it. Naturally, I decided to
run Fabio as a <a href="https://git.sr.ht/~damien/infrastructure/tree/master/jobs/fabio.nomad.erb">Nomad
job</a> too, but due to the
nature of load balancing, it has tighter restrictions for how it can run. Most jobs, including the
web server itself, don&rsquo;t actually care which nodes they run on, but load balancers need their host
to be registered with DNS. This means that we need the nodes themselves to know whether they are
intended to run a load balancer or not.</p>
<p>Nomad provides a number of filtering options for jobs including custom metadata, but I decided to go
with the <a href="https://www.nomadproject.io/docs/configuration/client#node_class"><code>node_class</code></a> attribute.
This is a custom value that you can asign each Nomad client explicitly for filtering purposes, and
has the added benefit over custom metadata of being included in node status output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>damien@support:~&gt; nomad node status
</span></span><span style="display:flex;"><span>ID        DC          Name                            Class          Drain  Eligibility  Status
</span></span><span style="display:flex;"><span>e9d5cdfe  ca-central  nomad-client-ca-central-UgcT5Q  load-balancer  false  eligible     ready
</span></span><span style="display:flex;"><span>67d7b064  ca-central  nomad-client-ca-central-4XMmYQ  &lt;none&gt;         false  eligible     ready</span></span></code></pre></div>
<p>Fabio jobs can then be specified to run exclusively on <code>load-balancer</code> nodes with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">constraint</span> {
</span></span><span style="display:flex;"><span>	attribute <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${node.class}&#34;</span>
</span></span><span style="display:flex;"><span>	value     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;load-balancer&#34;</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<h3 id="dns-management">DNS Management</h3>
<p>Once the <code>load-balancer</code> node is up and running an instance of Fabio, everything should
<em>technically</em> be available on the internet, but it won&rsquo;t be very easy to reach without a domain
name. However, it would also be a pain to manually update a DNS management system with new records
every time your cluster changes.</p>
<p>Fortunately, DNS records can be considered just another part of your infrastructure, and can
therefore be provisioned with Terraform! This means that any time a new <code>load-balancer</code> node is
created or destroyed, a DNS record is created or destroyed along with it, automatically keeping your
domain name in sync with available load balancers.</p>
<p>To support this, I defined a Terraform module called
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/terraform/domain-address"><code>domain-address</code></a>,
which takes as input the domain, a name for the record, and a list of Linode instances. The
<code>linode_domain_record</code> resource can then be used to define <code>A</code> and/or <code>AAAA</code> records pointing to the
IPv4 and/or IPv6 addresses respectively:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;linode_domain&#34; &#34;d&#34;</span> {
</span></span><span style="display:flex;"><span>  domain <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">domain</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;linode_domain_record&#34; &#34;a&#34;</span> {
</span></span><span style="display:flex;"><span>  for_each    <span style="color:#f92672">=</span> toset(terraform.workspace <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;default&#34;</span> <span style="color:#960050;background-color:#1e0010">?</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">instances</span>[<span style="color:#960050;background-color:#1e0010">*</span>].<span style="color:#66d9ef">ip_address</span> <span style="color:#960050;background-color:#1e0010">:</span> [])
</span></span><span style="display:flex;"><span>  domain_id   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">linode_domain</span>.<span style="color:#66d9ef">d</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  name        <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">name</span>
</span></span><span style="display:flex;"><span>  record_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;A&#34;</span>
</span></span><span style="display:flex;"><span>  target      <span style="color:#f92672">=</span> <span style="color:#66d9ef">each</span>.<span style="color:#66d9ef">value</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;linode_domain_record&#34; &#34;aaaa&#34;</span> {
</span></span><span style="display:flex;"><span>  for_each    <span style="color:#f92672">=</span> toset(terraform.workspace <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;default&#34; ? [for ip in var.instances[*].ipv6 : split(&#34;/&#34;</span>, <span style="color:#66d9ef">ip</span>)[<span style="color:#ae81ff">0</span>]] <span style="color:#960050;background-color:#1e0010">:</span> [])
</span></span><span style="display:flex;"><span>  domain_id   <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">linode_domain</span>.<span style="color:#66d9ef">d</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  name        <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">name</span>
</span></span><span style="display:flex;"><span>  record_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AAAA&#34;</span>
</span></span><span style="display:flex;"><span>  target      <span style="color:#f92672">=</span> <span style="color:#66d9ef">each</span>.<span style="color:#66d9ef">value</span>
</span></span><span style="display:flex;"><span>}</span></span></code></pre></div>
<p>One thing to note here is the <code>terraform.workspace</code> check within the <code>for_each</code> line. This is to
support development flows that use <a href="https://www.terraform.io/docs/state/workspaces.html">Terraform
workspaces</a>, which can be useful for testing
cluster changes (such as OS upgrades) without affecting the existing deployment. DNS records are
global, so we use this check to ensure that they are only created within the default workspace and
aren&rsquo;t overwritten to point to a non-production cluster.</p>
<h3 id="cert-renewals">Cert Renewals</h3>
<p>The last step is to set up automatic SSL certificate renewal. If you don&rsquo;t need or want to serve
your website over HTTPS, then you can skip this step, but most websites should probably be served
securely and therefore will need SSL.</p>
<p>In addition to providing orchestration for always-on services, Nomad supports something akin to cron
jobs in the form of the
<a href="https://www.nomadproject.io/docs/job-specification/periodic.html"><code>periodic</code></a> stanza. With this, we
can write a Nomad job that executes our SSL renewal regularly so that its validity never lapses.</p>
<h4 id="getting-the-certificate">Getting the Certificate</h4>
<p>The first step is deciding which SSL renewal service and tool to go with. <a href="https://letsencrypt.org/">Let&rsquo;s
Encrypt</a> is the big name in this space because it&rsquo;s free and run by a
nonprofit, but that&rsquo;s not a hard requirement as long as whichever service you choose has APIs for
automatic renewal.</p>
<p>For tool, I decided to go with <a href="https://github.com/acmesh-official/acme.sh">acme.sh</a>, because it
provides a nice interface with minimal dependencies, though there are a number of <a href="https://letsencrypt.org/docs/client-options/">other
options</a> available for any ACME-compatible service.</p>
<h5 id="the-challenge">The Challenge</h5>
<p>The ACME protocol requires you to be able to prove that you own the domain being renewed through a
<a href="https://tools.ietf.org/html/rfc8555#section-8">challenge</a>, with the two main options being HTTP
and DNS. HTTP challenges work by giving you some data and verifying its existence under
<code>http://&lt;domain&gt;/.well-known/acme-challenge/</code>; DNS challenges work similarly, but the
challenge expects the data to be available as a TXT record on the domain.</p>
<p>Due to the distributed nature of jobs running on Nomad, the HTTP challenge is not really viable, so
I recommend using the DNS challenge along with your DNS provider&rsquo;s API, such as
<a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi#cloud-manager"><code>dns_linode_v4</code></a>.</p>
<h5 id="storing-certificates-in-vault">Storing Certificates in Vault</h5>
<p>Fabio supports loading SSL certificates from
<a href="https://github.com/fabiolb/fabio/wiki/Certificate-Stores#vault-kv">Vault</a>, so after the challenge
succeeds, that&rsquo;s where we will want to save the cert and key. However, this also becomes a little
tricky due to the distributed nature of this job, since Vault will be running on a different server.</p>
<p>Fortunately, Vault has an HTTP API, so as long as we have the address of at least one available
Vault server and a valid token, the job can send an HTTPS request with the new cert and key
contents. With <code>acme.sh</code>, this takes the form of specifying <code>--reloadcmd</code> and a script like
<a href="https://git.sr.ht/~damien/infrastructure/tree/master/artifacts/vault-write-certs.sh"><code>vault-write-certs.sh</code></a>,
which can be made available as a downloadable artifact that Nomad will make available to the renewal
job.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>The architecture described in this post was born out of a desire to better understand how cluster
networking works, a general interest in HashiCorp tools and philosophies, and a purely pragmatic
desire to be able to avoid cloud vendor lock-in when needed.</p>
<p>This post was written over the span of several months and ended up being pretty long, so it may not
be the easiest to read, and some of its links may also fall out of date as I continue making updates
to my architecture.</p>
<p>If you find anything that&rsquo;s broken, or you just have a question or comment, feel free to shoot me an
<a href="mailto:blog@damienradtke.com?subject=RE:%20Building%20a%20Cloud-Free%20Hashistack%20Cluster">✉️ email</a>.</p>
<!-- raw HTML omitted -->

  </div>

    </main>

    
  </body>
</html>

A public/post/building-gtk-applications-like-websites/index.html => public/post/building-gtk-applications-like-websites/index.html +403 -0
@@ 0,0 1,403 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
<head>
  <link href="//gmpg.org/xfn/11" rel="profile">
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <meta name="generator" content="Hugo 0.115.1">

  
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>Building GTK4 Applications Like Websites &middot; Version 7.0</title>

  
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/print.css" media="print">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/poole.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/syntax.css">
  <link type="text/css" rel="stylesheet" href="https://damienradtke.com/css/hyde.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Abril+Fatface|PT+Sans:400,400i,700">


  
  <link rel="apple-touch-icon-precomposed" sizes="144x144" href="/apple-touch-icon-144-precomposed.png">
  <link rel="shortcut icon" href="/favicon.png">

  
  
</head>

  <body class=" ">
  <aside class="sidebar">
  <div class="container sidebar-sticky">
    <div class="sidebar-about">
      <a href="https://damienradtke.com/"><h1>Version 7.0</h1></a>
      <p class="lead">
       Fixes many known issues with version 6.0 
      </p>
    </div>

    <nav>
      <ul class="sidebar-nav">
        <li><a href="https://damienradtke.com/">Home</a> </li>
        <li><a href="/pages/about"> About </a></li><li><a href="https://github.com/dradtke/"> Projects — Github </a></li><li><a href="https://git.sr.ht/~damien"> Projects — Sourcehut </a></li>
      </ul>
    </nav>

    <p>&copy; 2023. All rights reserved. </p>
  </div>
</aside>

    <main class="content container">
    <div class="post">
    <h1>Building GTK4 Applications Like Websites</h1>
    
      <time datetime=2022-05-24T00:00:00Z class="post-date">Tue, May 24, 2022</time>
    

    <figure class="regular"><img src="https://imgs.xkcd.com/comics/installing.png"/>
</figure>

<p>The modern web, broadly, consists of two distinct innovations:</p>
<ol>
<li>The technology for <strong>rendering</strong> and <strong>interacting</strong> with web pages (also known as the trifecta
of HTML, CSS, and JavaScript)</li>
<li>The client-server <strong>deployment model</strong>, which allows a single desktop application (your web
browser) to alter its behavior using instructions sent over the network by a web server</li>
</ol>
<p>Whether you love it or hate it, web rendering and interaction technology is widely used, both on the
web and off. Electron and React Native take the web technology stack and deploy it to your desktop
or phone, respectively, as a standalone application that don&rsquo;t require a (separate) browser.</p>
<p>By contrast, the client-server deployment model is not widely used outside of the web in which it
originated. Desktop application stacks, such as GTK (which is the focus of this post), only run on
the client.</p>
<p>In this post, I want to demonstrate a technique for building GTK applications like they were
websites, using the client-server deployment model.</p>
<p>(Full source code for the prototype, along with examples, can be seen at
<a href="https://github.com/dradtke/gtk-webby">https://github.com/dradtke/gtk-webby</a>)</p>
<h2 id="but-why">&hellip;but why?</h2>
<p>Mainly because web browsers are, first and foremost, document renderers; their role as application
runtimes came much later. JavaScript was famously invented by Brendan Eich in just <a href="https://thenewstack.io/brendan-eich-on-creating-javascript-in-10-days-and-what-hed-do-differently-today/">10
days</a>,
and the rate at which new frameworks are released has caused many developers to suffer from
<a href="https://auth0.com/blog/how-to-manage-javascript-fatigue/">JavaScript fatigue</a>. This deluge of
frontend frameworks largely stems from browsers being designed back in the 90&rsquo;s to do one thing, and
now being used to do so much more. By contrast, development stacks such as GTK were designed to run
applications from the start, and as a result, come with many useful features that web applications
need to either build from scratch, or import from a third-party library.</p>
<p>However, GTK applications lack the web&rsquo;s flexibility. When an update is made to a GTK application,
all users need to download the update and install it; when an update is made to a web application,
it does not require any installation, and the changes are immediately available for all possible
clients (after a refresh, of course).</p>
<p>Also, GTK already has support for markup-based rendering in the form of UI files, so we don&rsquo;t have
to build any of the rendering from scratch. This vastly simplifies the work involved, and means that
the task is mostly one of gluing together existing components, rather than building something new.</p>
<h2 id="so-youre-basically-building-a-new-browser">So you&rsquo;re basically building a new browser?</h2>
<p>Kind of. The goal is to have users download a single application that behaves similarly to
browsers, but one that instead renders native GTK interfaces rather than HTML.</p>
<p>(It is worth noting that GTK does support <a href="https://docs.gtk.org/gtk4/broadway.html">broadway</a>, which
allows you to access a running application remotely through your browser, but in this post I want to
explore the possibility of building client-server applications with fully native GTK)</p>
<p>So, that&rsquo;s the goal. In order to get there, we need to break it down into (some of) the individual
features provided by your average web browser:</p>
<ol>
<li><a href="#rendering">Rendering</a></li>
<li><a href="#scripting">Scripting</a></li>
<li><a href="#linking">Linking</a></li>
<li><a href="#styling">Styling</a></li>
<li><a href="#submitting-forms">Submitting Forms</a></li>
<li><a href="#page-title">Page Title</a></li>
</ol>
<p>After that, I&rsquo;ll end on</p>
<ol start="7">
<li><a href="#missing-features">Missing Features</a></li>
<li><a href="#final-thoughts">Final Thoughts</a></li>
</ol>
<h1 id="rendering">Rendering</h1>
<p>The most basic, fundamental feature we need is the ability to render an application. In order to do
that, we first need a canvas:</p>
<figure class="regular"><img src="/images/building-gtk-applications-like-websites/webby.png"/>
</figure>

<p>This is Webby, the current name of my proof-of-concept, built with <a href="https://gtk-rs.org/">gtk-rs</a>.</p>
<p>When you first launch it, it doesn&rsquo;t look like much, but that&rsquo;s because it&rsquo;s an empty shell. In
order to get it to do something, we need to also build a web application that we can access.</p>
<p>Sticking with our Rust theme, we can build one pretty quickly using Rocket:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#75715e">#[macro_use]</span> <span style="color:#66d9ef">extern</span> <span style="color:#66d9ef">crate</span> rocket;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[get(</span><span style="color:#e6db74">&#34;/&#34;</span><span style="color:#75715e">)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">index</span>() -&gt; <span style="color:#66d9ef">&amp;</span>&#39;static <span style="color:#66d9ef">str</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#e6db74">r</span><span style="color:#e6db74">#&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;interface&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;object class=&#34;GtkBox&#34; id=&#34;body&#34;&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                &lt;property name=&#34;orientation&#34;&gt;vertical&lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                &lt;property name=&#34;halign&#34;&gt;start&lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                &lt;child&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                    &lt;object class=&#34;GtkButton&#34;&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                        &lt;property name=&#34;label&#34;&gt;Click Me&lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                    &lt;/object&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                &lt;/child&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;/object&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/interface&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;#</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#[launch]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">rocket</span>() -&gt; <span style="color:#a6e22e">_</span> {
</span></span><span style="display:flex;"><span>    rocket::build().mount(<span style="color:#e6db74">&#34;/&#34;</span>, routes![index])
</span></span><span style="display:flex;"><span>        .configure(rocket::Config{
</span></span><span style="display:flex;"><span>            port: <span style="color:#ae81ff">8000</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">..</span>Default::default()
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>With this running, we can now navigate to <code>http://localhost:8000/</code> and see what we get:</p>
<figure class="regular"><img src="/images/building-gtk-applications-like-websites/webby-hello.png"/>
</figure>

<p>In a nutshell, here is what&rsquo;s happening:</p>
<ol>
<li>We are making a GET request to <code>http://localhost:8000/</code>, which returns the user interface
definition returned by our Rocket application.</li>
<li>The body of the request is parsed by a GTK
<a href="https://docs.gtk.org/gtk4/class.Builder.html">Builder</a>, which will instantiate all of the
described objects.</li>
<li>We look for a <a href="https://docs.gtk.org/gtk4/class.Widget.html">Widget</a> object with id <code>body</code> (here
it is an instance of <a href="https://docs.gtk.org/gtk4/class.Box.html">Box</a>).</li>
<li>That <code>body</code> widget is set as the child of Webby&rsquo;s content area, which is simply a
<a href="https://docs.gtk.org/gtk4/class.ScrolledWindow.html">ScrolledWindow</a> instance.</li>
</ol>
<p>GTK Builders are very powerful, so this is all that we need in order to properly render an interface
of basically arbitrary complexity. By using <a href="https://glade.gnome.org/">Glade</a>, you can pretty
quickly build fairly complex interfaces, and anything contained within a <code>body</code> widget will be
rendered by Webby.</p>
<p>EDIT: Apparently Glade is <a href="https://blogs.gnome.org/christopherdavis/2020/11/19/glade-not-recommended/">not
recommended</a> and will
likely not support GTK4. Its replacement is called
<a href="https://flathub.org/apps/details/ar.xjuan.Cambalache">Cambalache</a>, though it is still experimental
so your results may vary. A lot of GNOME developers appear to simply write Builder XML by hand,
at least until the tooling in this space stabilizes.</p>
<p>In order to really do something with this, we need to introduce some additional features.</p>
<h1 id="scripting">Scripting</h1>
<p>Continuing with the example above, in order for the button to do anything, we need to handle its
<code>clicked</code> signal.</p>
<p>The Builder way of doing this would be to define a <code>&lt;signal&gt;</code>
<a href="https://docs.gtk.org/gtk4/class.Builder.html#signal-handlers-and-function-pointers">element</a>, but
that requires that your handler be defined within the application, and frankly I&rsquo;m not sure how that
works when not using GTK&rsquo;s native C.</p>
<p>The web&rsquo;s solution here is to introduce scripting (where JavaScript comes in), which allows the web
server to specify client-side behavior. We can do something similar by bringing in an embeddable
scripting language. I&rsquo;ve chosen Lua because it&rsquo;s relatively simple and easy to embed, though Webby
theoretically can support other languages too, even
<a href="https://github.com/denoland/rusty_v8">JavaScript</a> itself if you really want to.</p>
<p>Now, the big caveat here is that the GTK UI interface format was not designed to support scripting,
or frankly to mimic the web. So in order to support scripting, we will need to &ldquo;extend&rdquo; the format
to support what we need.</p>
<p>For reference, in order to run a script on a regular web page, you could put a tag like this in your
HTML:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text/javascript&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">log</span>(<span style="color:#e6db74">&#34;hello world&#34;</span>);
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">script</span>&gt;
</span></span></code></pre></div><p>The approach taken by Webby is very similar:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;web:script</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;lua&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>  print(&#34;hello world&#34;)
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/web:script&gt;</span>
</span></span></code></pre></div><p>There are a couple key differences here:</p>
<ol>
<li>Webby uses a <code>web:</code> prefix to identify tags that are considered extensions to the UI format. When
loading an interface description, Webby will strip out any tag with this prefix before passing it
to the Builder, since it will throw an error when it encounters an unrecognized tag. There are a
few supported <code>web:</code> tags (more on that later), and <code>web:script</code> is used to indicate the presence
of a script that should be executed.</li>
<li>The <code>type</code> attribute is required, and specifies the name of the scripting language in a plain,
non-MIME format. Only <code>lua</code> is supported, but this provides an easy extension point for adding
new languages.</li>
</ol>
<p>Using this capability, here is how we might connect a signal handler to our button:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;UTF-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;interface&gt;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">&lt;!-- Script tags can be placed anywhere, so why not at the top? --&gt;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">&lt;web:script</span> <span style="color:#a6e22e">type=</span><span style="color:#e6db74">&#34;lua&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>	  button = find_widget(&#34;click-me&#34;)
</span></span><span style="display:flex;"><span>	  button:connect(&#34;clicked&#34;, false, function()
</span></span><span style="display:flex;"><span>	  	alert(&#34;hello world&#34;)
</span></span><span style="display:flex;"><span>	  end)
</span></span><span style="display:flex;"><span>	<span style="color:#f92672">&lt;/web:script&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GtkBox&#34;</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;body&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;orientation&#34;</span><span style="color:#f92672">&gt;</span>vertical<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;halign&#34;</span><span style="color:#f92672">&gt;</span>start<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;child&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;object</span> <span style="color:#a6e22e">class=</span><span style="color:#e6db74">&#34;GtkButton&#34;</span> <span style="color:#a6e22e">id=</span><span style="color:#e6db74">&#34;click-me&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;property</span> <span style="color:#a6e22e">name=</span><span style="color:#e6db74">&#34;label&#34;</span><span style="color:#f92672">&gt;</span>Click Me<span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/child&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/object&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/interface&gt;</span>
</span></span></code></pre></div><p>Now when we click the button, we get this:</p>
<figure class="regular"><img src="/images/building-gtk-applications-like-websites/alert.png"/>
</figure>

<p>Neat! In order to facilitate this, we need to initialize the Lua virtual machine with a few things:</p>
<ol>
<li>Two global functions: <code>alert()</code> and <code>find_widget()</code>.</li>
<li>A custom <code>Widget</code> user data type, which is returned by <code>find_widget()</code>.</li>
<li>A <code>connect()</code> method on the <code>Widget</code> type.</li>
</ol>
<p>Other functions are available as well (notably <code>widget:get_property()</code> and <code>widget:set_property()</code>),
so there&rsquo;s quite a bit of flexibility in what you can do.</p>
<h1 id="link