In March 2021 I first posted about setting up a Pi cluster. Initially, I had tried (and subsequently failed) to set up a full-fledged Kubernetes (k8s) cluster. Then, I discovered k3s, a lightweight distribution of Kubernetes designed for edge environments (which also works on ARM devices). It ships with an embedded sqlite3 database as default storage when setting up a server node but it is trivial to use etcd3/MySQL/PostgreSQL if desired. I was very pleased with how simple the k3s launcher is and it made the entire installation experience straightforward.
I based my installation on the single-server setup with an embedded database documented by Rancher. In my configuration, node1
is running k3s in server
mode and node[2:6]
is running it in agent
mode. Here’s a breakdown of what I had to do to install k3s.
Note During the installation process I encountered this error:
[INFO] Failed to find memory cgroup, you may need to add "cgroup_memory=1 cgroup_enable=memory" to your linux cmdline (/boot/cmdline.txt on a Raspberry Pi)
To resolve the issue, I added the recommended flags to the linux cmdline as the error message suggests on each node before installing k3s.
On node1
I installed k3s.
curl -sfL <<<<<<<<https://get.k3s.io>>>>>>>> | sh -
Then, I started k3s in server
mode:
k3s server
The other nodes were even easier. When it’s installed and run, k3s will check for K3S_URL
and K3S_TOKEN
environment variables. If they are set, then k3s assumes this is a worker node and starts up in agent
mode. As root
, I copied the token value from node1:/var/lib/rancher/k3s/server/node-token
and used the following command to install k3s and automatically register the node with the server running on node1
(full token values omitted):
curl -sfL <<<<<<<<https://get.k3s.io>>>>>>>> | K3S_URL=<<<<<<<<https://node1:6443>>>>>>>> K3S_TOKEN=K109...f2bb::server:bc1...2e9 sh -
I checked out the results of my installation on node1
using k3s kubectl get nodes
.
Without using SEO tags to serve metadata for social media, links will appear plainly and without much to draw the eye. I wanted to change how my blog post presents on social media, so I searched for how to add SEO tags to my existing blog. Since my blog is built on GitHub Pages and thus uses Jekyll, I was able to install the jekyll-seo-tag
plugin. Here’s how.
In the Gemfile
of the Jekyll project add the following line:
gem 'jekyll-seo-tag'
The Gemfile
is used when testing my blog locally using bundle exec jekyll serve
.
In the _config.yml
of the project, find the plugins_dir
section and add the following line:
- jekyll-seo-tag
A GitHub Actions workflow will execute builds and include the jekyll-seo-tag
plugin now configured in _config.yml
when generating the static assets during the build process.
My blog is a fork of gh-pages-blog. jekyll-seo-tag
requires frontmatter to be added to two files, located in the _includes/head
directory. I simply added the following frontmatter expression in the <head>
tags of page.html
(which represents the parent blog) and post.html
(which represents any individual post):
{ % seo % }
NOTE I added spaces on either side of the Liquid expression (the braces and percent symbol) to avoid Jekyll from replacing it with the SEO data it generates during the build process of the blog. Check out more about static files from Jekyll.
After this change in the static files, I am now able to add an image
property to any post to specify a social card. Any post without a value for image
will use a default social card, which is also the card used for the parent blog. Posts are written in Markdown (GFM) and the property values are written in a comment header. An example below is how Jekyll knows what to call this post.
---
layout : post
title : Using the jekyll-seo-tag plugin
category : social
image : "/seo/2022-01-11.png"
---
I put all this together thanks to this very helpful blog post from Meagan Waller.
These pages are useful for testing meta tags:
Swagger is a suite of tools to generate and represent RESTful APIs. It promotes the OpenAPI 2.0 spec which describes an API using JSON representation in a specification file called swagger.json
. Since YAML is a superset of JSON, the specification file can conform to YAML.
In my projects, I want to be able to generate swagger.json
from an annotated codebase to serve using Swagger UI.
go-swagger
is a CLI tool. The following is an example command for generating a Swagger spec
swagger generate spec -o ./swagger.json
Based on the installation docs Swagger UI can be served as part of an application using plain HTML/CSS/JS.
The folder /dist
includes all the HTML, CSS and JS files needed to run SwaggerUI on a static website or CMS, without requiring NPM.
/dist
folder to your server.index.html
in your HTML editor and replace “««««https://petstore.swagger.io/v2/swagger.json”»»»» with the URL for your OpenAPI 3.0 spec.Reading and writing code is a big part of my daily life and the opportunity to put things together in Go is a fun break from the enterprise application development I do at C Spire. Swagger UI is a tool we use in our Spring/Spring Boot applications to easily share our API documentation with each other and the teams which integrate with us. This post is my personal documentation for how to integrate a tool I know and love in a new programming language.
u := config.AuthCodeURL(oauthState)
prepares the URL, a call to http.Redirect()
accepts u
and the server redirects the browser).r.FormValue("code")
) via the redirection URL provided in step 1.Download the oauth2 library for import.
go get golang.org/x/oauth2
The app structure I followed is pretty simple:
main.go
use net/http
to start a servermain.go
package main
import (
"fmt"
"log"
"net/http"
"foundry-automations/handlers"
)
func main() {
server := &http.Server{
Addr: fmt.Sprintf(":8000"),
Handler: handlers.New(),
}
log.Printf("Starting HTTP Server. Listening at %q", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("%v", err)
} else {
log.Println("Server closed!")
}
}
/handlers
and add two .go filesbase.go
<service>.go
where <service>
is a meaningful representation of the authorization server you are relying on (e.g., google apis)In base.go
create func New() http.Handler {}
and set up a mux
with two handlers, "/auth/<service>/login"
and "/auth/<service>/callback"
, then return mux
. The helper functions passed as the second argument of mux.HandleFunc
will live in your <service>.go
implementation
base.go
package handlers
import (
{
"net/http"
)
func New() http.Handler {
mux := http.NewServeMux()
// Root
mux.Handle("/", http.FileServer(http.Dir("templates/")))
// // OauthGoogle
// mux.HandleFunc("/auth/google/login", oauthGoogleLogin)
// mux.HandleFunc("/auth/google/callback", oauthGoogleCallback)
mux.HandleFunc("/auth/pco/login", pcoLogin)
mux.HandleFunc("/auth/pco/callback", pcoCallback)
return mux
}
}
I am continuing to learn more about Go and its history.
The language follows idioms.
One of the most important aspects of my job is writing meaningful error messages. It’s something a lot of people don’t ever stop to consider: where do error messages come from?
Software developers create tools used by others and by nature of the broad interconnected tooling systems in computers and the internet at-large it cannot be difficult to imagine how collaboration is vitally important. Error messaging is how a software developer communicates with their users about expected and unexpected behavior. I believe that software which does not provide meaningful error messages (here’s what went wrong, here’s how to fix it) is not well-written software.
Error handling is a fundamental skill for software engineering.
Review the following go
packages for an example of returning an error. Source from golang.org
greetings.go
package greetings
import (
"errors"
"fmt"
)
// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// If no name was given, return an error with a message.
if name == "" {
return "", errors.New("empty name")
}
// If a name was received, return a value that embeds the name
// in a greeting message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}
main.go
package main
import (
"fmt"
"log"
"example.com/greetings"
)
func main() {
// Set properties of the predefined Logger, including
// the log entry prefix and a flag to disable printing
// the time, source file, and line number.
log.SetPrefix("greetings: ")
log.SetFlags(0)
// Request a greeting message.
message, err := greetings.Hello("")
// If an error was returned, print it to the console and
// exit the program.
if err != nil {
log.Fatal(err)
}
// If no error was returned, print the returned message
// to the console.
fmt.Println(message)
}
log
and errors
packages are golang builtinserrors.New(UNEXPECTED_ERROR)
or errors.New(EXTERNAL_SERVICE_ERROR)
however Go does not support this pattern since it alternates between camelCase and PascalCase to denote variables which are global to a file vs. global to a package (respectively)