Compare commits

..

1 Commits

Author SHA1 Message Date
Jack Jackson
25d351add5 First attempt
Not great - text overruns the boxes unless a limited height is set, but
varying heights of headers leads to uneven lengths of content.
2023-05-17 21:06:34 -07:00
77 changed files with 171 additions and 2943 deletions

99
.drone.yml Normal file
View File

@ -0,0 +1,99 @@
kind: pipeline
name: hello-world
type: docker
platform:
os: linux
arch: arm64
steps:
- name: block-posts-containing-tk
image: busybox
commands:
# # This is necessary because, if `grep ...` doesn't find anything, it will _return_ (not print) a value of 1
# # (non-zero return codes indicating errors in Unix - since there are many more ways for something to go wrong
# # than there are for it to go right!), and so the `files=` assignment will also return 1, and the whole operation
# # will be considered a failure.
# #
# # Since a non-zero value is truthy in Linux, we can use the OR operator (`||`) to only execute the second command
# # if the first one errors out. So, this line can be translated to English as:
# # "Set the variable `files` to a list of all the files that contain `TK` - unless there aren't any, in which case
# # set it to `FILES NOT FOUND"
- files=$(grep -rl 'TK' blog/content/posts || echo "FILES NOT FOUND")
# # We have to filter out (`grep -v`) the "marker" value of `FILES NOT FOUND`, otherwise the no-matches case would
# # be recorded as having 1 matching file, leading to an error-out below.
# # (I guess _technically_ there's an edge case in that, if I ever make a blog post titled "FILES NOT FOUND" _which also_
# # contains the string `TK`, it would slip through this check. But that feels pretty unlikely - not least because spaces
# # are very rare in my filesystem names - so I'm ok taking that risk)
- count=$(wc -l <(echo "$files" | grep -v "FILES NOT FOUND") | awk '{print $1}')
- if [[ "$count" -gt "0" ]]; then
- echo "Found TK in $count files:"
- echo $files
- exit 1 # TODO - and alerting via Matrix!
- fi
- name: build-blog
image: alpine
# Very unlikely to need updates, and pulling images seems slow on this setup -
# can manually reset this if necessary
pull: if-not-exists
commands:
# I considered caching this install in a pre-built image in registry,
# but the install seems pretty quick!
- apk add hugo git
- git submodule init
- git submodule update --recursive
- hugo --source blog
- name: push-built-image
image: plugins/docker
settings:
registry: gitea.scubbo.org
repo: gitea.scubbo.org/scubbo/blog_helm
tags: ${DRONE_COMMIT_SHA:0:10}
debug: true
launch_debug: true
username: scubbo
password:
from_secret: gitea_password
settings:
mtu: 1450
- name: auto-update-infra-repo
image: gitea.scubbo.org/scubbo/auto-repo-update-drone-plugin:latest
settings:
branch: main
git_repo: https://gitea.scubbo.org/scubbo/blog-infrastructure
image_tag: ${DRONE_COMMIT_SHA:0:10}
destination_file: helm/deployed-images/prod
author_email: scubbojj@gmail.com
author_name: Jack Jackson
commit_message: "[Auto] Update Prod to ${DRONE_COMMIT_SHA:0:10}"
access_token:
from_secret: gitea_pat
- name: update-argo
image: curlimages/curl
environment:
ARGO_TOKEN:
from_secret: argo_token
commands:
- "curl -s -X POST \"argo-cd-argocd-server.argo.svc.cluster.local/api/v1/applications/blog-infrastructure/sync\" -H \"Authorization: Bearer ${ARGO_TOKEN}\" -H \"Content-Type: application/json\""
- name: purge-cache
image: curlimages/curl
environment:
CLOUDFLARE_TOKEN:
from_secret: cloudflare_token
commands:
- "curl -s -X POST \"https://api.cloudflare.com/client/v4/zones/c86d55d225ed973d5da45239beac2f99/purge_cache\" -H \"Authorization: Bearer ${CLOUDFLARE_TOKEN}\" -H \"Content-Type:application/json\" -d '{\"files\":[\"https://blog.scubbo.com\"]}'"
- name: telegram_notification
image: appleboy/drone-telegram
when:
status:
- failure
- success
settings:
token:
from_secret: telegram_token
to:
from_secret: telegram_convo_id
image_pull_secrets:
- dockerconfigjson

View File

@ -1,44 +0,0 @@
name: Mirror to GitHub
run-name: Mirror to GitHub
on: [push]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
# It's still _so_ baffling to me that this doesn't appear to be available in the `github` context.
- name: Determine repo name
run: |
echo "Test output"
echo ${{ github.repository }} | cut -d'/' -f2
echo "REPO_NAME=$(echo ${{ github.repository }} | cut -d'/' -f2)" >> "$GITHUB_ENV"
echo
echo "from braces"
echo "${{ env.REPO_NAME }}"
echo "$REPO_NAME"
- name: Import Secrets
id: import-secrets
uses: hashicorp/vault-action@v2
with:
url: http://vault.avril
method: kubernetes
role: act-runner-helm-charts
secrets: |
github/token?org_name=${{ gitea.repository_owner }} token | GITHUB_TOKEN ;
- name: Commit Report Sync
uses: https://gitea.scubbo.org/scubbo/commit-report-sync@main
with:
source_repo_domain: gitea.scubbo.org
source_repo_owner: ${{ github.repository_owner }}
source_repo_name: ${{ env.REPO_NAME }}
target_repo_domain: github.com
target_repo_owner: scubbo
target_repo_name: gitea-commit-mirror
token_for_target_repo: ${{ steps.import-secrets.outputs.GITHUB_TOKEN }}
- name: Revoke GitHub token
run: |
curl -s -H "Authorization: Bearer ${{ env.GITHUB_TOKEN }}" -X DELETE https://api.github.com/installation/token
echo "Token revoked"

View File

@ -1,101 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions! 🚀
on: [push]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
# Despite not being present in the QuickStart instructions, this step is necessary in order to install `node`,
# which is itself required for `actions/checkout`
# Cannot use `actions/setup-node` because that _itself_ requires `node` (it makes it available to user-code,
# rather than to the system. Interesting discussion [here](https://gitea.com/gitea/act_runner/issues/538))
- name: Install node
run: apt-get update && apt-get install -y nodejs
- name: Check out repository code
uses: actions/checkout@v4
- name: Block posts containing-tk
run: |
# This is necessary because, if `grep ...` doesn't find anything, it will _return_ (not print) a value of 1
# (non-zero return codes indicating errors in Unix - since there are many more ways for something to go wrong
# than there are for it to go right!), and so the `files=` assignment will also return 1, and the whole operation
# will be considered a failure.
#
# Since a non-zero value is truthy in Linux, we can use the OR operator (`||`) to only execute the second command
# if the first one errors out. So, this line can be translated to English as:
# "Set the variable `files` to a list of all the files that contain `TK` - unless there aren't any, in which case
# set it to `FILES NOT FOUND"
files=$(grep -rl 'TK' blog/content/posts || echo "FILES NOT FOUND")
# We have to filter out (`grep -v`) the "marker" value of `FILES NOT FOUND`, otherwise the no-matches case would
# be recorded as having 1 matching file, leading to an error-out below.
# (I guess _technically_ there's an edge case in that, if I ever make a blog post titled "FILES NOT FOUND" _which also_
# contains the string `TK`, it would slip through this check. But that feels pretty unlikely - not least because spaces
# are very rare in my filesystem names - so I'm ok taking that risk)
count=$(wc -l <(echo "$files" | grep -v "FILES NOT FOUND") | awk '{print $1}')
if [[ "$count" -gt "0" ]]; then
echo "Found TK in $count files:"
echo $files
exit 1 # TODO - and alerting via Matrix!
fi
- name: Build blog
# We need the `extended` version of Hugo to support CSS transpilation via `libsass`. CBA to migrate to the new
# `dartcss` tool.
run: |
wget https://github.com/gohugoio/hugo/releases/download/v0.139.0/hugo_extended_0.139.0_linux-amd64.deb
sudo dpkg -i hugo_extended_0.139.0_linux-amd64.deb
apt-get update && apt-get install -y git
git submodule init
git submodule update --recursive
hugo --source blog
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: gitea.scubbo.org
username: scubbo
password: ${{ secrets.PAT_FOR_GITEA_ACCESS }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
gitea.scubbo.org/scubbo/blogcontent:latest
gitea.scubbo.org/scubbo/blogcontent:${{ gitea.sha }}
update-deployment-repo:
runs-on: ubuntu-latest
needs:
- build-and-push
steps:
- name: Check out deployment repo
uses: actions/checkout@v4
with:
repository: scubbo/blog-deployment
token: ${{ secrets.PAT_FOR_GITEA_ACCESS }}
# Seems insecure - and it is! - but this is literally recommended by the Kustomize docs
- name: Install kustomize
run: curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
- name: Update deployment
run: |
./kustomize edit set image image_name=*:${{ gitea.sha }}
- name: Commit and push
run: |
git config --global user.email "auto-updater@scubbo.org"
git config --global user.name "UpdaterBot"
git add --all -- ':!kustomize'
git commit -m "Update blog content to ${{ gitea.sha}}"
git push

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
blog/public
blog/resources
!.gitea/

View File

@ -17,6 +17,5 @@ Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
[Link-text]({{< ref "/posts/name-of-post" >}})
-->

View File

@ -2,6 +2,7 @@ baseURL = 'https://blog.scubbo.org/'
languageCode = 'en-us'
title = "Jack Jackson's Blog"
theme = "ananke"
disqusShortname = 'scubbo-blog'
SectionPagesMenu = "main"
@ -16,11 +17,9 @@ SectionPagesMenu = "main"
github = "https://github.com/scubbo"
linkedin = "https://www.linkedin.com/in/jack-jackson-14a47441/"
mastodon = "https://fosstodon.org/@scubbo"
rss = "https://blog.scubbo.org/index.xml"
custom_css = ['inlinespoiler.css']
show_reading_time = true
page_source_base = "https://gitea.scubbo.org/scubbo/blogContent/src/branch/main/blog/"
DisqusShortname = 'scubbo-blog'
# That is, explicitly disabling `categories` taxonomy
# https://gohugo.io/content-management/taxonomies/#example-removing-default-taxonomies

View File

@ -12,8 +12,4 @@ Opinions expressed are solely my own and do not express the views or opinions of
About me
--------
I'm an engineer at LegalZoom, and I spend a fair chunk of my free time tinkering with technology too.
I'm particularly interested in resiliency, continuous verification, and self-hosting.
You can see my code projects (including this blog's source code) at my [self-hosted Gitea instance](https://gitea.scubbo.org).
I'm an engineer at LegalZoom, and I spend a fair chunk of my free time tinkering with technology too. I'm particularly interested in resiliency, continuous verification, and self-hosting.

View File

@ -2,7 +2,6 @@
title: "2021 in Books"
date: 2021-12-30T08:29:09-08:00
tags:
- end-of-year-wrapups
- reading
---
My good friend [George](https://tinyletter.com/altthoughtprocess/) set himself a challenge a while back to read 52 books in a calendar year. He succeeded (as George is wont to do), and that achievement has always stuck in my mind as impressive[^1]. I don't think I'd _ever_ be able to equal it (especially not now, with Work-From-Home removing my most common reading time - the commute), but I did start tracking my reading as a matter of interest. To that end, I present my year-of-reading-in-review, with book-by-book recaps and the [full list]({{< ref "#full-list" >}}) at the end:

View File

@ -2,8 +2,8 @@
title: "2022 Wrap Up"
date: 2022-12-31T12:47:00-08:00
tags:
- end-of-year-wrapups
- productivity
- real-life
- reading
---

View File

@ -1,9 +0,0 @@
---
title: "2023 Advent of Code"
date: 2023-12-01T18:43:13-08:00
tags:
- programming-challenges
- rust
---
Just a quick note to record that, for the first time, I'm taking part [Advent Of Code](https://adventofcode.com/) - a series of programming challenges that run every day from the 1st to the 25th of December. Inspired by my [experiences]({{< ref "/posts/short-thoughts-2023-03-05" >}}) during [Exercism's 12in23 challenge](https://exercism.org/challenges/12in23), I'll be trying to complete the challenges in Rust (see my solutions [here](https://github.com/scubbo/advent-of-code-2023)). That's on top of trying to complete Exercism's "_December Diversions_", as well as 5 challenges in _another_ language for the year-long badge to make up for the fact that January was unassigned (and keeping up my reading of [Ward](https://www.parahumans.net/) to remain on-track to finish by the end of the year). It's going to be a busy month!

View File

@ -1,137 +0,0 @@
---
title: "2023 Wrap Up - Articles"
date: 2024-01-01T15:31:18-08:00
tags:
- end-of-year-wrapups
- productivity
- SDLC
---
Stand-outs among articles I read this year - abandoning the table layout from last year in favour of readability.
<!--more-->
If you just want the best-of-the-best, I recommend [The Categories Were Made For Man, Not Man For The Categories](https://slatestarcodex.com/2014/11/21/the-categories-were-made-for-man-not-man-for-the-categories/), [Many More Much Smaller Steps](https://www.geepawhill.org/2021/09/29/many-more-much-smaller-steps-first-sketch/), and [Five Geek Social Fallacies](https://plausiblydeniable.com/five-geek-social-fallacies/) (with an honourable mention for [Should You Split That File](https://www.pathsensitive.com/2023/12/should-you-split-that-file.html) if you are a software engineer - the others are of general interest, even though MMMSS _focuses_ on engineering)
## [Theory-building and why employee churn is lethal to software companies](https://www.baldurbjarnason.com/2022/theory-building/)
([HN](https://news.ycombinator.com/item?id=34328069))
_Software Development is not just the act of writing code (we knew that!), but the creation of an accurate and up-to-date mental model of the system - a model which is useful for understanding how to alter that system in response to desired changes. Corollaries are that deep understanding of the context and intention of a system is essential, that team churn is catastrophic, and rewrites-for-the-sake-of-rewrites are less self-indulgent than they may seem, as they (re-)generate this familiarity._
I'm reminded of the surprising (but correct!) claim that it's preferable to have a broken-but-easy-to-change system, than a working-but-hard-to-change one. Firstly, the system may _actually_ be broken without you knowing; secondly, even if it is working perfectly _now_, requirements may change _tomorrow_. This article is also particularly relevant in the context of my team's work to establish ownership (which includes "_understanding of the subject_") of systems at work.
## [Velocity defeats itself. Get acceleration instead](https://jessitron.com/2022/12/22/velocity-defeats-itself-get-acceleration-instead/)
([HN](https://news.ycombinator.com/item?id=34353907))
_Do not neglect software engineering work which reduces the friction of development or experimentation_
Hardly a novel perspective, but I liked this way of phrasing it, especially since it stresses the hard-upper-limit of "force" as an input to acceleration.
## [The Categories Were Made For Man, Not Man For The Categories](https://slatestarcodex.com/2014/11/21/the-categories-were-made-for-man-not-man-for-the-categories/)
_Starts off with a good abstract exploration of how categories should be created so as to be useful, but are neither authoritative/immutable nor inherently matters of fact; moves on to apply that, sensibly, to transgender identity._
I was _very_ cautious of reading anything from the EA/Rationalist space, especially anything related to gender identity, as they have a certain reputation - but surprisingly this piece prioritized harm-reduction over the mastubatory satisfaction of retroactively declaring one's prejudices as "supported by science"
## [Social Justice and Words, Words, Words](https://slatestarcodex.com/2014/07/07/social-justice-and-words-words-words/)
A discussion of how the same terms can be used by the same person to mean different things - often with the outcome (intentional or not) that an indefensible claim can be made defensible by "retreating" to an alternative definition when challenged.
Another Rationalist article - this one a little less sympathetic, but certainly not inconsistent or obviously-wrong. I will say that his experience of Social Justice appears to have been more hostile than mine, but I also 100% believe that such people/experiences exist. Certainly, it's not a stretch to believe that some SJ folks take good intentions to harmful extremes, or subscribe to absolutist philosophies which admit no nuance.
This ties into a theme I've noticed recently, of disagreements persisting not because of differing views, but differing definitions of the terms being used in the argument. If I say "_X isn't racist_", and you and I have differing definitions of what "racist" means, we're going to struggle to make progress in understanding until we discover that mismatch. Along the way, we'll be hampered in reaching that understanding if there are value judgements associated with many of these differingly-defined terms - if you think you're hearing me say "_I see the harm being done by X, but I don't care about it_", when what I really mean is "_I recognize the harm that X does, but that harm doesn't fall under the category of 'racism'_", then our discussion will be fraught!
## [Of Dogs And Lizards: A Parable Of Privilege](https://sindeloke.wordpress.com/2010/01/13/37/)
A straightforward description (linked from the preceding article) of how Privilege arises through differing experiences/backgrounds, and how to react to that. I am a _big_ fan of social justice proponents (hmm, we're overdue a new term for that, aren't we?) who prioritize spreading understanding and education rather than criticism and shame - famously poor recruitment tools. In my opinion very few people (not zero!) are deliberately or actively cruel, but many are either lazy or small-minded. In an ideal world, people would "_do the right thing_" simply because it's right, not just because you tell them how their patterns could harm people they care about - but, since we don't live in an ideal world, it's sensible to consider prioritizing harm-reduction over ideological purity[^victims-dont-owe-you-anything].
## [Communicating Like A Grown-Up](https://fs.blog/carl-braun-communicating/)
([HN Link](https://news.ycombinator.com/item?id=35355372)) For a while, I have been convinced that clear written communication is one of the greatest superpowers that a human can possess. It is the best way we have to disseminate and preserve information; and while it may take more effort to write a persistent long-form piece than to make a convincing verbal argument, that effort will be justified by the ability to share and refer-to the durable copy, and by the higher-quality thinking generated by an inability to hide behind easy speech. In fact, a determination to improve my own writing was one of the [primary motivators for this blog in the first place](https://blog.scubbo.org/posts/my-first-post/).
Plenty of good advice in this piece. A consistent theme I want to pull out is that, in any collaboration, we should always be of the mindset that we're all doing out best and pulling in the same direction. Assuming ill intent or stupidity is never a useful strategy - whether you're right or wrong, you will have made an enemy, and if you're wrong you'll make yourself look foolish. Much better to assume that information has not been fully distributed, or that incentives are misaligned - approaches which gracefully allow your collaborator to change their position without admitting defeat. In all cases, prioritize finding the best outcome for the team, not "winning".
## [The Copenhagen Interpretation Of Ethics](https://web.archive.org/web/20220705105128/https://blog.jaibot.com/the-copenhagen-interpretation-of-ethics/)
(Archive.org link because the original is no longer available) "_The Copenhagen Interpretation of Ethics says that when you observe or interact with a problem in any way, you can be blamed for it. At the very least, you are to blame for not doing **more**._"
This is a really interesting one. _Prima facie_, this interpretation seems to be nonsense - we should celebrate _any_ amount of improvement or amelioration of a problem, even if the response doesn't completely fix the problem (so long as the "improvement" doesn't push someone into a local maximum from which it would be hard to escape to genuine long-term betterment). And yet, all the examples that the article gives of the phenomenon - of organizations taking steps which provide limited-but-non-zero assistance, with zero downside - feel, for want of a better word, "icky".
You could argue that the first example (paying homeless people to carry Wifi equipment) _did_ have a downside in that it was dehumanizing to the participants, but, well...they're free to reject the offer if they (justifiably!) value their pride more than the associated payment. Sure, consent cannot be freely given when under duress, and you could argue that a homeless or otherwise-needy person isn't able to express their economic preferences accurately when offered compensation that would (however temporarily) stave off hardship[^capitalism-yo] - that is, that they're compelled to accept an offer which they would otherwise refuse - but, the Wifi-providing company isn't the one _responsible for_ that person's hardship or for the existence of poverty, so why should they be condemned for offering _a choice_ wherein the negative option is "_remain in the situation you would be in if this choice wasn't offered_"?
As I say, an interesting one - something I don't have a well-defined attitude to, yet, but that I do at least want to keep in mind in the future.
(Also, "[Philosophy Bro](http://www.philosophybro.com/archive/peter-singers-drowning-child-argument)" is just delightful)
## [The Seven Ur-Programming Languages](https://madhadron.com/programming/seven_ur_languages.html)
([HN Link](https://news.ycombinator.com/item?id=35813496)) I have been fascinated with the different properties and priorities of Programming Languages since reading "Seven Languages In Seven Weeks" a few years ago. This was a fascinating read on some alternative perspectives on PL design.
(I still don't grok `Lisp`s, though 😅 I can recognize that they _are_ elegant and concise and all that good stuff, but the "Summer of Sexps" was one of the more frustrating months of [Exercism's 12in23](https://exercism.org/challenges/12in23) challenge. I just missed named variables too much!)
## [Many More Much Smaller Steps](https://www.geepawhill.org/2021/09/29/many-more-much-smaller-steps-first-sketch/)
(First read a while ago, but I neglected to note it down at the time)
In my previous role, I half-jokingly compiled a Wiki page of short general-purpose statements which one could use to simulate me as a chatbot - randomly selecting one of the statements as a response in a conversation would do a good job of impersonating me. [Manual Work Is A Bug](https://queue.acm.org/detail.cfm?id=3197520) and [Code Only Says What It Does](https://brooker.co.za/blog/2020/06/23/code.html) were among the list, and this article belongs alongside them.
(Honourable mention to [Choose Boring Technology](https://boringtechnology.club/), which is referenced [a little later](#how-do-you-keep-us-innovative-i-dont) as well)
## [Make It Easy To Do The Right Thing](https://amcaplan.ninja/blog/2016/12/30/make-it-easy-to-do-the-right-thing/)
The title is a mantra of mine - particularly since I've moved to an SRE role - and the article provides some good examples of the concept.
## [Five Geek Social Fallacies](https://plausiblydeniable.com/five-geek-social-fallacies/)
A collection of social dysfunctions that geeks susceptible to, most likely due to a some combination of a. a history of being ostracized, and b. finding socializing unintuitive and thus trying to "learn" it as a skill.
One of those articles that, once you read it, you will notice the tendencies and patterns everywhere (probably including, uncomfortably, in your own past behaviour).
## [How Do You Keep Us Innovative? I Don't](https://ntietz.com/blog/forefront-of-innovation/)
An alternative view on [Choose Boring Technology](https://boringtechnology.club/). It is a fallacy common to junior engineers to think that engineers are in the business of writing code, and that writing cool/efficient/modern/clean/exciting (delete as appropriate) code is what they are paid for. This is not the case. Engineers are paid to solve problems. If you can solve the problem _well_ (and sutainably) with a few lines of hacky bash scripting rather than an elegant multifaceted framework-y masterpiece, that is perfectly acceptable - and often optimal. Newer tools are, by-definition, less-proven than the battle-tested dinosaurs - they represent risk. That risk must be outweighed by significant power or functionality elsewhere.
(An second-order effect is that using newer flashier technologies _might_ be a positive recruiting consideration, but I suspect that that effect is negligible except in areas which are themselves cutting-edge - AI, Self-driving, etc.)
## [Should You Split That File?](https://www.pathsensitive.com/2023/12/should-you-split-that-file.html)
([HN Link](https://news.ycombinator.com/item?id=38489485)) A neat approach for code organization, sensibly balancing the competing pressures when developing code and considering the cognitive-load aspects inherent in them. I am a sucker for solutions which identify a spectrum of solutions which lie along some line of trade-offs, and which then ask "_what if we could take some of the best of both worlds_"? (Yes, I'm still trying to learn Rust 😝)
I do think it's interesting to note how some comments argue in favour of larger files because they allow for (say) searching within a single file in a single operation, when "_searching across a whole project_" _is_ a single operation in well-set-up IDEs (fight me, Vimsters). This feels like a local maximum - to what should we keep using sub-optimal practices because the optimal practice is sub-optimal to those using sub-optimal tools?[^vim-is-an-ide]
Bonus points because the HN comments include this classic form-of-comment: [1](https://news.ycombinator.com/item?id=38490257), [2](https://news.ycombinator.com/item?id=38490757).
## Others
Articles that I thought were worth recording at the time, but which I didn't think warranted a paragraph-length write-up above. The snippets are not (usually) direct quotes but rather are my summations.
* [How To Do Hard Things](https://every.to/no-small-plans/how-to-do-hard-things) ([HN](https://news.ycombinator.com/item?id=35425897#35427605)) - A blend of mindfulness meditation (awareness of thoughts and aversion, recognition that thoughts are real but not necessarily true, center yourself in the present and the body, etc.) and therapy/productivity theory (let actions arise from values, form habits with routine).
* [Everything Is A Practice](https://luxagraf.net/essay/everything-is-a-practice) ([HN](https://news.ycombinator.com/item?id=35437062)) - "_You are what you repeatedly do. Do things that lead you down a path you want to be on. Don't neglect a diversity of skills._" (not a new realization!)
* [Systems Design: What We Hope We Know](https://apenwarr.ca/log/20230415) ([HN](https://news.ycombinator.com/item?id=35580776)) - "_Engineering as a discipline of compromise (Science as a discipline of mechanistic testing of inspired hypotheses), Insight as a new-to-you realization, latency vs. throughput_"
* [Resisting Deterministic Thinking](https://zephoria.medium.com/resisting-deterministic-thinking-52ef8d78248c) - "_Outcome are almost never certain - pick the choice that best shapes the probability-space to your desired outcomes._" (shared by [George](https://www.georgelockett.com/))
* [We Don't Trade With Ants](https://worldspiritsockpuppet.com/2023/01/10/we-dont-trade-with-ants.html) ([HN](https://news.ycombinator.com/item?id=36265774)) - an interesting take on how Human/AI relations might evolve
* [The JavaScript Gom Jabbar](https://frantic.im/javascript-gom-jabbar/) ([HN](https://news.ycombinator.com/item?id=36564010)) - _shudder_
* [Consider SQLite](https://blog.wesleyac.com/posts/consider-sqlite), via [How I Run My Servers](https://blog.wesleyac.com/posts/how-i-run-my-servers). Reminiscent of a [great talk by David Crenshaw](https://www.youtube.com/watch?v=RqubKSF3wig) on the topic of ruthlessly prioritizing simple functionality in your tools - though, to be clear, I still find GoLang to be an extraordinarily-frictiony language for no apparent good reason (unlike Rust, for instance, which is also incredibly-frictiony _but with the friction intentionally introduced with deliberate trade-offs_)
* [Don't Mess With A Genius](https://shreevatsa.wordpress.com/2010/06/04/dont-mess-with-a-genius/) - fun historical story about Isaac Newton's time as Master Of The Mint.
* [Shamir Secret Sharing](https://max.levch.in/post/724289457144070144/shamir-secret-sharing-its-3am-paul-the-head-of) - an engineering war story
* [That Time I Built Excel For Uber And They Ditched It Like A Week After Launch](https://basta.substack.com/p/no-sacred-masterpieces) ([HN](https://news.ycombinator.com/item?id=37527720)) - "_Every piece of code you write as an engineer is legacy code. Maybe not right now, but it will be. Someone will take joy in ripping it out someday._" - plus a cool tidbit of how Excel works (read and find out!)
* [Doing A Job](https://govleaders.org/rickover.htm) - an exploration of the idea of ownership of (loosely - pride and competency in) a role.
[^victims-dont-owe-you-anything]: though even this straightforward formulation can be fraught, as it leans close to an oft-maligned claim that it is the responsibility of the oppressed to educate their oppressors, and to do so in conciliatory and flattering ways - an unreasonable ask, when the oppressed are so often dealing with plenty of other exhausting hardships and trauma. I have a longer blog post planned on this general idea; but in short, this seems like an instance of a special case of "_miscommunication via differing definition_" - explicitly, the miscommunication which arises when making a normative statement (an expression of desirability - "_you should do X_" or "_it's a good idea to do X_") without expressing the criteria used for judgement. If your aim is to change the behaviours of your listeners, then a position which castigates them is less likely to be successful than one which explains to them the benefits of the change - _but_ a victim has, in fact, no responsibility to try to directly effect that change, and not all their speech should be judged through that lens. Their speech might be intended simply to vent, or to build community with other victims, or to effect change _indirectly_ by changing the opinion of large demographics or of policy-makers which will put pressure on their oppressors.
[^capitalism-yo]: In a broader sense, neither can _anyone_ in a capitalist society outside of the 1% - but that's a whole tangent...
[^vim-is-an-ide]: FWIW, I'm _sure_ that Vim could be set up to do "Project Search" just as well as IntelliJ/VS Code/whatever could do. Rest assured, if I'm talking about "Vim as a substandard IDE", then I'm not talking about _your_ setup 😝
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,169 +0,0 @@
---
title: "2023 Wrap Up - Books"
date: 2023-12-26T12:02:15-08:00
tags:
- end-of-year-wrapups
- reading
---
Another End Of Year Wrap-up, focusing (as the [previous]({{< ref "posts/2022-wrap-up" >}}) [installations]({{< ref "posts/2021-in-books">}}) did) initially on reading[^affiliate-links].
<!--more-->
# Recaps
Potential spoilers, of course, for all the books (check [here](#full-lists-and-stats) to see the full title list), though IMO I'm keeping them minimal - thematic rather than narrative.
I rarely write notes on book _as_ I'm reading them, so please take review comments with a grain of salt - I may be misremembering!
## The Year Of Sanderson
I knew that [Brandon Sanderson's Four Secret Projects](https://www.kickstarter.com/projects/dragonsteel/surprise-four-secret-novels-by-brandon-sanderson) were going to be the focus of the year, and they didn't disappoint. The least complimentary thing I can say about them is that Frugal Wizard felt too YA _for me_, but me not being the target audience doesn't make it a bad book. Yumi felt a little under-explained and -explored (surprising, for the king of well-established internally-consistent magic systems), but I still teared up a little at the appropriate emotional moments - and I _loved_ seeing more of an established character. The Sunlit Man was a compelling action romp with a cool hook even without the _tantalizing_ snippets of the broader Cosmere story; and TOTES (hehe), similarly, would be a delightful "_reverse Princess Bride_" even if you're not someone who geeks at the sight of the word "Hoid".
I'm lumping White Sands in here too even though it wasn't part of the Secret Projects. Probably my least favourite of the Cosmere books, which still puts it in rare company. It was perfectly servicable both narratively and Cosmere-structurally, I just wasn't particularly grabbed at any point.
## The Laundry Files
9 of this years books were from The Laundry Files, "_a series of novels \[that\] mix the genres of Lovecraftian horror, spy thriller, science fiction, and workplace humour_", recommended by [George](https://www.georgelockett.com). They had some high points - "_what if an Investment Bank Analyst Pod, but Vampires_", in particular, was executed much more competently than just a series of lazy jokes about how finance/capitalism is blood-sucking and evil (though, to be clear...) - but overall I have zero desire either to finish the series (after a big in-world event leads to a significant character-focus shift), or to go back and re-read. Perfectly servicable leisure reading, though!
(As a point of comparison, despite the numerous flaws of the Dresden Files, I will be picking up those books at release date right up 'til the end of the planned Big Apocalyptic Trilogy finale)
## The First 90 Days
As I began my new position with LegalZoom - the first time I'd changed companies as a mature, deliberate, somewhat-thoughtful somewhat-adult, rather than as a fresh-faced college grad in no position to be intentional about his career trajectory - I figured it behooved me to study some Real Adult Grown-up Techniques for starting a job off on the right foot.
It was definitely useful and I'm glad I read it, though the target demographic was more the "_decision-making executives_" cohort than the "_primarily IC/executors_" group that I'm in. Don't get me wrong, Senior+ Engineers _should_ be decision-makers (and good ones need to be political too - sadly), and the strategies for understanding the org and for discovering the "hidden org-chart" were valuable, but the heavy focus on understanding phase and segment of the business were less relevant to someone without significant input on hiring, external product selection, or funding. Still - I'm all for cross-training, it was neat to understand how execs think about such things, and I'm sure I'll make use of the knowledge at some strange time in the future!
Very grateful to ex-coworker Kyle for lending this to me!
## Underdog
An alpha-read of a draft novel by my cousin Nick. A thoroughly enjoyable semi-apocalyptic YA fantasy story, with hints of Dune. Trope-y in the best ways, a lot of fun. Looking forward to buying the published version!
## The Rust Book
I've been working through Exercism's [12in23](https://exercism.org/blog/12in23-calendar) challenge this year, and Mechanical March's challenge of working with Rust got me fascinated with the language that so many have spoken so enthusiastically about[^golang-sucks]. I resolved to work through [The Rust Book](https://doc.rust-lang.org/stable/book/) this year to deepen my understanding - realistically, 5 quick coding challenges barely gives you an understanding of the idioms of the language or how it really operates on a daily basis.
The book was reasonably well-written, but knowledge didn't really sink in - I came away from each chapter feeling _reasonably_ confident that the concepts made sense and that I could explain them, and then would invariably score a zero on the "check understanding" quiz that followed. I had (and still do have, as I work through [Advent Of Code]({{< ref "/posts/2023-advent-of-code" >}})) real difficulty translating the theoretical ideas into practical applications. I suspect it's something that will get easier with practice, as I rewire my brain to naturally think about concepts like borrowing. I got a recommendation for a book which is apparently better for learning practical application ("_Programming Rust_" by Blandy, Orendorr, and Tindall), which I'll work through next year.
## The Infinite And The Divine
The release of the WH40k Magic The Gathering decks last year resparked my interest in the sprawling lore of the mega-setting. I'd heard good things both about this book, and the "Twice-Dead King" series by Nate Crowley (a friend-of-a-friend), so I figured I'd give it a go.
Perfectly servicable low-brain-energy entertainment. Nothing to write home about, but enjoyable to speed through as an balm to the heavy thinking of The Rust Book!
## Project Hail Mary
Spiritual-sequel to The Martian, this is another Competence-Porn In Space book, though with a little more characterization at the cost of some of the clever MacGuyvering. I did enjoy the smatterings of linguistics and sociology at the start, but from about halfway through it loses focus and can't decide if it wants to be a buddy-story or a story about Cool Science, and ends up suffering as both. If you are desperate for more of The Martian, this will scratch that itch, though it's a little inferior in most ways.
## A Long Way To A Small, Angry Planet
A recommendation from ex-coworker Amanda. "Cosy Sci-Fi" - take the multi-species interstellar-alliance structure of The Culture or Mass Effect, downgrade the tech to the level of (and apply the "_found family on a boat-in-space_" flavour of) Firefly, and round off the sharp edges (no disrespect! Pleasant comfy fiction is valid and worthwhile!).
## Domain-Driven Design
...OK, I'll admit, this was a spite-read. I have a coworker with whom I have a...challenging relationship, primarily because of our differing methods of communication - and for reasons of professionalism, I will go no further than that! For several months, he insisted on crowbarring concepts and quotes from this book into _every_ discussion, until I eventually resolved to read it to understand what he was talking about.
For all that I read it for less-than-ideal reasons, it was a good read! Suffered somewhat from "[Seinfeld Is Unfunny](https://tvtropes.org/pmwiki/pmwiki.php/Main/SeinfeldIsUnfunny)" syndrome (warning - TVTropes link), in that many concepts in it seemed self-evidently correct and barely even worth stating - the curse of an influential and insightful book is that, ~10 years after it's published, everything in it will seem "obvious" because everyone will have adopted its ideas! Still, putting a structure on and vocabulary around the ideas is valuable.
On the positive side, I now have a better understanding of what my coworker means when he drops buzzwords without elaboration!
## A Fire Upon The Deep
A standard of "best sci-fi" books, I had high hopes for this one - hopes that, sadly, were not met. It had some cool ideas in it, for sure (the composite-consciousness species was a new one to me), but other than that - well, I'll just quote my [Mastodon post](https://fosstodon.org/@scubbo/110618207602838405)[^no-toots] of the time:
> "A Fire Upon The Deep" may be the most disappointing book I've ever read. The intro teased truly inhuman AI viewpoints, never delivered; popular reporting of the book makes much of "code archaeology", only mentioned once in passing; the two alien races are interesting ideas but nothing's done with them (I truly thought the radio-coats would lead to Flenser *becoming* a Power); the ending was a non-event that left countless plot threads open.
It's _possible_ that the sequel Children Of The Sky would pick up on some of those plot threads in a satisfying way, but I'm unlikely to chance it. Who knows, though - maybe going in with lowered expectations would make me enjoy it more!?
A comparison with Blindsight seems fitting, here. Both are space-based stories which use post-/in-human species as a way to examine assumptions about consciousness and personhood - but, by deliberately having almost _no_ story (or, rather - having a story which was purely a vehicle for "_cool discoveries about the puzzle/structure in question_"), Blindsight avoided any necessity to have a _satisfying_ story. AFUTD _tried_ to have a compelling story with characters we cared about, and (to me) failed.
## Naming Things
Classically, [one of the hardest problems in computer science](https://martinfowler.com/bliki/TwoHardThings.html). This was a short but worthwhile read: very little in here that was truly unknown (aside from the term "_polyseme_", for "_a symbol \[word\] with multiple related meanings_"), but, like "_Effective Java_", "_The Elements Of Style_", or [Oblique Strategies](https://en.wikipedia.org/wiki/Oblique_Strategies), it will be a good collection of tie-breaking advice for those moments when I have a niggling feeling that something _could_ be done better but I need a clearly articulated explanation of how (and why it's better).
(And, yes, I fully recognize that EJ and TEoS are both pretty out-dated by now. I'm not claiming that _every_ piece of advice they give is good and correct - but, at the very least, a clearly-articulated argument that you disagree with will help you formulate your own argument!)
## Ward
Hoo boy. OK, this is the biggie 😅
Several years before I started this blog, I read [Worm](https://parahumans.wordpress.com/), a infamous web serial which was, at the time, nearly as long as the entire Song Of Ice And Fire:
![Length of various sci-fi series](/img/length-of-worm.jpg "I think the most surprising thing here is that, since this diagram was made [in 2018](https://old.reddit.com/r/Parahumans/comments/8nyhqi/worm_length_compared_to_other_sff_series/), there's only been one Stormlight Archive book :P")
I've described it previously as "_A gritty grounded superhero story - like if Brandon Sanderson and George R. R. Martin collaborated to entirely reinvent the MCU as a sci-fi story rather than 'a soap opera with punching'_". It certainly wasn't perfect - not all of that length was free from filler, and the author has some irritating linguistics quirks that begin to grate very quickly[^used-their-power] - but when it was good, oh my, it was _fantastic_. The superpowers were thoughtfully created (and limited, and combined/conflicted) and widely varied; the characterisation was _masterful_ given what a broad cast of characters the author introduced (some for only a few paragraphs at a time, but with barely a placeholder character among them - they all felt like _people_); and, while not every arc was gold, on balance they were excellent, and the ones that were good were _incredible_. Seriously, this book - which, I remind you, was released for free, chapter-by-chapter, onto the Internet by an amateur - contains several of the most vivid, shocking, and compelling scenes I've ever read. It's not a whole-hearted universal recommendation - I've already acknowledged that the pacing and prose are patchy, and it would honestly be faster to list the Content Warnings that _don't_ apply to the book than those that do[^content-warnings] - but if this sounds up your street, it probably is.
Fast-forward to last year, when I saw some Mastodon posts from someone who'd just finished Ward, the sequel to Worm, and was waxing lyrical about it. I planned to read it in 2024 as I would be dedicating this year to Sanderson, but I ended up having enough time to complete Ward this year.
It was great, veering on excellent! That's certainly a step-down from my breathless praise of the original, which is intentional - it had its moments that stand alongside the original, and fleshed out the cosmology/power-system a little, but rarely measured up. Partly this was just the standard problem of sequels - after introducing such an awesome and epic (word choice intentional!) world, anything else would feel like a let-down. That said, the story itself felt disjointed - Worm certainly had distinct arcs, but there still felt like a coherent through-thread, or at least that there was a smooth transition from one to the other. Ward's arcs, by contrast, simply...ended, and then started anew. The protagonist, too, cannot hold a candle to Worm's magnificent anti-hero Taylor. A standout of the original was the way that the narrator's _intensely_ biased (but internally-consistent!) viewpoint is so compelling that you find yourself going along with their reasoning and justifications until you take a step back and realize how far down the slippery-slope she (and you) have slipped. By contrast, Victoria certainly has a hang-up that needs dealing with - but a) it's just the one issue, b) she's _way_ more justified than Taylor ever was (though goes about it in a sometimes-unhelpful way), and c) her viewpoint is uncritically supported by almost every character, so the conflict between perception and reality isn't foregrounded. Finally, the ending felt confused and rushed - I honestly still don't understand the intention or stakes of one of the primary dramatic scenes. Again, comparing with Worm which is my favourite book-ending, and my favourite any-media ending except The Good Place, that's a let-down. Implication from the tone of some of the comments was that the author had gotten bored of the story and just wanted it done with so that he could move on, which I believe.
(On that point, I made the decision to read this book on the author's website, rather than using the "download to ePub" scripts that fans have created (as I did for Worm), so that I could read the comments as I went. Definitely a good decision - there are some _very_ smart (and obsessed!) people out there who gave some insightful commentary and discussion that really enhanced my enjoyment of the book. Again, for free - the Internet can be a wonderful place sometimes!)
Still, for all those flaws - a capital-G Great work, and one I'm very glad to have read. Where Worm is one that I don't-actively-recommend, this I think I would actively-(mildly-)dissuade someone from reading - so that it would only be read by those who are hooked enough to persevere despite discouragement, who I think are those most-likely to enjoy it.
...now I want to re-read Worm with comments...maybe in a couple years...
## The Lathe Of Heaven
I picked this up in a second-hand bookstore in Chicago during a trip for a conference - I'd been meaning to read more Ursula Le Guin for a while (I read The Earthsea Quartet as a kid, and The Left Hand Of Darkness a few years back), and seeing this by the checkout prompted me to pick it up. It's often on Best Of Sci-Fi lists - surely I'd enjoy it, right?
Again, as with A Fire Upon The Deep - heightened expectations lead to disappointment. It was fine, but I just didn't _get_ it. Scenes were described, a mechanic was introduced, but I just didn't get the _point_ of the book. I worried that I'd missed some subtle metaphor, as I did with Camus' "_The Plague_"[^the-plague], but no - from looking up reviews and responses, it seems that the story _is_ the story. I'm really not sure what it's trying to say - "_be careful what you wish for_", or "_power corrupts_", or (surely not!?) "_don't try to improve anything_" (the protagonist himself seems to hold this position and is presented sympathetically, which is, as the youth would say, a big yikes from me)?
## The Internet Con
I closed out the year with Cory Doctorow's latest book, subtitled "_How to Seize The Means Of Computation_". Nothing in here is new to anyone who hangs out on the same kinds of social media as me - I get the impression that this is a book intended to be bought-and-gifted (or, less charitably, read for the sense of navel-gazing self-congratulation). Which, y'know, nothing wrong with that!
# Full lists and stats
(Uncounted but acknowledged - finished Rhythm of War re-read)
1. Tress Of The Emerald Sea
2. White Sands
3. The Jennifer Morgue
4. The Tyranny Of Faith
5. The Fuller Memorandum
6. The Apocalypse Codex
7. The Rhesus Chart
8. The Annihilation Score
9. The Nightmare Stacks
10. The Frugal Wizard's Handbook For Surviving Medieval England
11. The Delirium Brief
12. The Labyrinth Index
13. The First 90 Days
14. Underdog
15. The Rust Book
16. The Infinite And The Divine
17. Dead Lies Dreaming
18. Project Hail Mary
19. A Long Way To A Small, Angry Planet
20. Domain-Driven Design
21. A Fire Upon The Deep
22. Yumi And The Nightmare Painter
23. Naming Things
24. The Sunlit Man
25. Ward
26. The Lathe Of Heaven
27. The Internet Con
I'd pre-acknowledged that this was not going to be a good year on either of my primary tracking stats ("_books by non-white non-male people_" and "_number of books read overall_"), what with a) Brandon Sanderson [spraying books all over the place](https://www.kickstarter.com/projects/dragonsteel/surprise-four-secret-novels-by-brandon-sanderson) and b) this being the year I finally tackled Ward. However, I actually ended up readding _more_ than I read last year. Whadda you know!? I guess indulging in The Laundry Files (which I could easily tear through at a consistent rate of a-book-a-week) pumped my numbers up a bit.
* 22 Fiction, 5 Non-Fiction ("_The First 90 Days_", "_The Rust Book_", "_Domain-Driven Design_", "_Naming Things_", "_The Internet Con_").
* No Genre analysis this year as too many are borderline. Most Branderson straddles the line between Fantasy and Sci-Fi too neatly to categorize, and The Laundry Files is positively allergic to picking a genre.
* 24 by Men, 2 by Women ("_A Long Way To A Small, Angry Planet_" and "_The Lathe Of Heaven_"), and 1 by a collaboration between a man and a woman "_with contributions from the Rust Community_".
* 1 Book by Friends-or-Family (level with last year)
# Summing up, and looking forward
I'd definitely like to read more non-fiction, and more books by non-white/non-men authors next year. Some that are planned off the top of my head (not necessarily prioritizing those criteria):
* OverLondon (based on [the author's delightful Mastodon presence](https://fosstodon.org/@georgepenney@sunny.garden), and a review I saw somewhere calling it Pratchett-esque).
* Thinking Fast And Slow - also picked up in the Chicago second-hand bookstore.
* The Design Of Everyday Things.
* "Programming Rust" by Blandy, Orendorr, and Tindall.
In the [previous year]({{< ref "/posts/2022-wrap-up" >}}), I included a round-up of the Articles I read that year, but this post is already getting overlong - I'll follow up with that in a separate post instead.
[^affiliate-links]: My Amazon Affiliate account expired from lack of use last year, which is unsurprising - well, it's actually surprising it lasted as long as it did! It seems rather fitting to abandon them in the same year I started my post-Amazon position.
[^golang-sucks]: Rust is often spoken of in opposition to GoLang, which is a big vote in Rust's favour to me. Hoo boy, there's a blog post in the works _there_, too...
[^no-toots]: Never say never, but I can't see myself ever using the word "Toot" unironically.
[^used-their-power]: Particularly egregious in a superhero story is the reverse-saidbookism of consistently using the phrase "_\[person\] used their power \[to do X\]_" rather than any alternative like "_\[person\] X'd_". Trust me, if you're talking about a character with flight powers, and you tell me that they floated/drifted/hovered to a location, I can figure out that they used their power to do so!
[^content-warnings]: Though, even there - while some truly horrific things happen, they rarely feel gratuitous - in the sense that I never got the feeling that the author thought "_hmm, I want to spice this scene up and keep people on their seat - let's have something horrible happen to Our Heroes_". Rather, the atrocities feel like coherent outcomes of the situation and of the vile-but-consistent villains that have already been established as having particular goals and Modi Operandi. "[_It's What My Character Would Do_](https://rpg.stackexchange.com/questions/37103/what-is-my-guy-syndrome-and-how-do-i-handle-it)" may not be an excuse in RPGs, but it's absolutely relevant in fiction.
[^the-plague]: To my shame, I got through the entire book (rather bemused!) before realizing that the plague was a metaphor for {{< inlinespoiler >}}Nazism/collaboration{{< /inlinespoiler >}}

View File

@ -1,108 +0,0 @@
---
title: "2024 Emails"
date: 2025-01-01T17:10:13-08:00
draft: true
extraHeadContent:
- <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js" integrity="sha512-ZwR1/gSZM3ai6vCdI+LVF1zSq/5HznD3ZSTk7kajkaj4D292NLuduDCO1c/NT8Id+jE58KYLKT7hXnbtryGmMg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
- <script src="https://cdn.jsdelivr.net/npm/moment@2.27.0"></script>
- <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@0.1.1"></script>
- <script src="/js/email-graph.js"></script>
tags:
- dataviz
- gtd
- productivity
---
On a whim, I started tracking my email volume during my morning startup routine during 2024.
<!--more-->
Specifically, for each of my email accounts (gmail and proton), I recorded the number of unread emails I had at the start of the morning, and how many remained unread at the end of the routine. The _ideal_ would be for the latter number to [always be 0](https://clean.email/blog/productivity/what-is-zero-inbox). Practically speaking, I tended to aim for ensuring that I had one fewer unread email in each account than I'd ended the previous day with - acknowledging that, since I wouldn't be checking my email every day (life gets in the way!), there would be regular spikes.
Anyway, here's ~~wonderwall~~ the graph[^chart-js]:
{{< rawhtml >}}
<canvas id="graph_canvas"></canvas>
{{< /rawhtml >}}
And filtered views of just each account
{{< rawhtml >}}
<canvas id="gmail_graph_canvas"></canvas>
{{< /rawhtml >}}
{{< rawhtml >}}
<canvas id="proton_graph_canvas"></canvas>
{{< /rawhtml >}}
I'm not sure what conclusions to draw from this, other than:
* I didn't check my email very much (or, at least, didn't track my checking) during May and June (unsurprising, as this was the time I was dealing with my Mum's passing-away)
* I received a _lot_ of emails in early July (again - unsurprising. This was mostly syncing up with interaction with the solicitors, funeral home, etc.)
* I'm receiving more email on my Gmail Account than my Proton. Unsurprising once again, as I've had that account for decades (as opposed to a year or so for Protonmail) and am probably on way more mailing lists that I should probably unsubscribe from, as well as being the account associated with various accounts and ecommerce sites.
I can't think why anyone _would_ - but if you want to see the code that generated this, it's here:
```python
#!/usr/bin/env python3
import os
import pathlib
import yaml
counts = {}
def main():
should_hide_gmail = os.environ.get('SHOULD_HIDE_GMAIL') == 'TRUE'
if not should_hide_gmail:
counts['gmail-start'] = []
counts['gmail-end'] = []
should_hide_proton = os.environ.get('SHOULD_HIDE_PROTON') == 'TRUE'
if not should_hide_proton:
counts['proton-start'] = []
counts['proton-end'] = []
d = pathlib.Path('/Users/scubbo/Dropbox/Obsidian/scubbo-vault/GTD/Daily TODOs')
for f_path in d.iterdir():
if f_path.is_dir():
continue
if not f_path.name.startswith('Todo - 2024'):
continue
with f_path.open('r') as f:
date = f_path.name[7:-3]
content = f.read()
data_index = content.index('# Data')
start_of_data_block = data_index+content[data_index:].index('```') + 3
length_of_data_block = content[start_of_data_block:].index('```')
data = yaml.safe_load(content[start_of_data_block:start_of_data_block+length_of_data_block])
if not should_hide_gmail:
if gs_count := data['gmail']['start-count']:
counts['gmail-start'].append({'date': date, 'count': gs_count})
if ge_count := data['gmail']['end-count']:
counts['gmail-end'].append({'date': date, 'count': ge_count})
if not should_hide_proton:
if ps_count := data['protonmail']['start-count']:
counts['proton-start'].append({'date': date, 'count': ps_count})
if pe_count := data['protonmail']['end-count']:
counts['proton-end'].append({'date': date, 'count': pe_count})
print([{'label': key, 'data': sorted(value, key=lambda x: x['date'])} for key, value in counts.items()])
if __name__ == '__main__':
main()
```
Output was piped to `pbcopy`, and then hard-coded into the JS that serves this page.
[^chart-js]: made with [Chart.js](https://www.chartjs.org/), which I'd already used in my [EDH ELO tracker](https://gitea.scubbo.org/scubbo/edh-elo).
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,142 +0,0 @@
---
title: "2024 Wrap Up Books"
date: 2025-01-01T14:40:11-08:00
tags:
- end-of-year-wrapups
- reading
---
Continuing a [trend]({{< ref "/tags/end-of-year-wrapups/" >}}) started way back in the [seventh post on this blog](https://blog.scubbo.org/posts/2021-in-books/), a summary of the books that I read this year.
<!--more-->
# Recaps
Potential spoilers, of course, for all the books described herein.
(All dates in American format. I'm sorry, I've succumbed)
## OverLondon - 2024-01-01 -> 01-07
[This book](https://www.overlondon.net/), discovered via [the author's Mastodon posts](https://fosstodon.org/@georgepenney@sunny.garden), felt reminiscent of Tom Holt or Robert Rankin, or a sillier zanier Pratchett (all high praise!). Fans of Failbetter's [Fallen London](https://www.fallenlondon.com/login) setting[^friends] will enjoy the Victoriana-rama of it all, though it's much more light-hearted.
## Use Of Weapons re-read - 01-16 -> 02-11
It's never a bad time to re-read one of Banks' greatest works. In this case, my Kindle happened to glitch out as I picked it up; it opened to a point where Zakalwe commandeers a civilian craft, and before I knew it I'd read all the way to the end and decided to restart from the beginning again.
I think my fondness for this book can be best expressed in the fact that [three]({{< ref "/posts/being-a-seagull" >}}) of [the]({{< ref "/posts/work-in-a-post-scarcity-utopia" >}}) [snippets]({{< ref "/posts/writing-poetry" >}}) that I chose to record during that brief blogging-period were from this book (which I also read in [2021]({{< ref "/posts/2021-in-books" >}})). A masterpiece.
## Thinking, Fast And Slow - 02-12 -> 03-12
I was cautious going into this one, as it had a reputation for being very "tech/finance/corporate-bro-y". The content can be (simplistically) summarized as "_how do people make choices, and how can you make your decision-making better for yourself (whatever that means)_?" Although I can certainly see how those demographics could use the ideas for their own nefarious purposes, there's plenty of benevolent things to learn here.
This was the first book[^notes] that I ever took notes on while I was reading it - a practice I highly recommend and will continue (for non-fiction). Looking back at the notes now, there are plenty of concepts that I had completely forgotten, but that spring back near-fully-formed on reading the summaries[^reliability-of-memory].
## Curse Of Strahd - 02-13 -> 03-16
The [sourcebook](https://en.wikipedia.org/wiki/Curse_of_Strahd) for the latest iteration of the most famous RPG campaign ever published. I've been running this campaign for 4 friends since July - despite 3 weddings and a bereavement among the participants, we've managed to keep up a respectable cadence.
For anyone thinking of running this for themselves, I cannot recommend highly enough [Strahd Reloaded](https://www.strahdreloaded.com/Introduction/A+DM's+Guide+to+Curse+of+Strahd). Before starting the game I scoured the Internet for extra content, DM's own twists, and fun ideas, when really I should have saved my time and just gone straight here. Pay the couple of bucks for the Discord membership and enjoy an transcendent experience on an already-excellent campaign.
## Trials Of Empire - 03-13 -> 03-24
Third book of the Empire Of The Wolf trilogy (writtten by acquaintance [Richard Swan](https://www.stonetemplelibrary.com/contact)), which starts with [The Justice Of Kings](https://www.amazon.com/Justice-Kings-Richard-Swan-ebook/dp/B096RTL1DN).
This series has always felt a little strangely-focused to me, in that it clearly has a staggeringly broad and deep setting (both in "breadth of the world" and "magical structure/system"), of which we only get to see the barest sliver. A valid choice, and one which leaves readers hungry for more, but - I'd still like to see a little more a little faster! Nonetheless, this was still an enjoyable read that I would have gladly picked up even without the friend-of-a-friend link.
## The Murderbot Diaries 1-5 - 03-25 -> 04-16
It would have felt a little disingenuous listing these as separate works, since I completed the first one in a single day. As I recall, only the last book really qualifies as a separate book, the first four are more properly short stories.
Despite having worked my way through so much content, these really didn't land for me. I can see how the protagonist's surly, laconic, cynical, tsundere-affection could be sympathetic and amusing, but I've had more than my fill of snark from...basically most fandoms I enjoyed as a youth, especially Whedon's ilk, webcomics like Something Positive[^something-positive], and many comics/video-games in general.
Nonetheless, I'm glad that people enjoy it! It's just not for me.
## Children Of Time/Ruin/Memory - 05-03 -> 06-01
This series had popped up on a bunch of "best sci-fi" lists, especially in the context of being more optimistic than most. I'm glad I gave it a try - although the first book is far-and-away the best, even the lowest point of this series is still a worthwhile read. If the hook of "_what would it look like for a race of spiders to be uplifted to human-level intelligence? How would their physical differences result in differing social and technological evolution?_" tickles your brain at all, you'll have a grand old time.
## Moving Pictures re-read - 06-12 -> 08-09
I picked this up for a little comfort while staying in my childhood home dealing with the abrupt decline and passing-on of my mother. Nothing much to say, here, other than RIP (or should I say "[GNU](https://wiki.lspace.org/GNU_Terry_Pratchett)") Mum - I love you and miss you.
## Wheel Of Time re-read 1-8 - 08-22 -> 12-15
I first read the Wheel Of Time around 2010, at which point it was still incomplete. I burned out on the series around book 9, after I realized that basic plot of the last three books followed the exact same template[^wheel-of-time-plot]. Having since discovered Brandon Sanderson and noted that he'd completed this series after the author's death, as well as having noted that Amazon has been producing a TV adaptation of the series (which might be something I'd enjoy watching with my wife, who enjoyed Game Of Thrones until it got bad), I resolved to reread-and-complete this pillar of modern fantasy.
...you guys, it's just so dull though? As a good friend (who introduced me to Branderson) said, "_Its just so boring. And feels like it was written 100 years ago_". Maybe this is an example of [Seinfeld Is Unfunny](https://tvtropes.org/pmwiki/pmwiki.php/Main/OnceOriginalNowCommon)[^original-name] (warning - TVTropes), but it started feeling like a slog around book 5 and didn't really improve.
Plus, I was strongly incentivized to quit (or at least pause) around this time, because...
## Wind And Truth (Stormlight 5) - 12-25 -> ...
...I'd intended to hold off on purchasing this book until I finished Wheel Of Time, to reduce temptation to quit; but then my wife told me that our local Barnes & Noble was having a going-out-of-business sale, and how could I resist?
![Many D&D accessories, a couple board games, and the book Wind And Truth](/img/barnes-and-noble-haul.jpg "My haul")
Only ~450 pages in so far, but loving it. The magic system is more varied, nuanced, and interesting than WoT's, the characters are better-developed and richer, and the cosmology is...well, it's the Cosmere, come on!
## Learning Domain-Driven Design // Practial Process Automation - ?? -> ??
Two books that were covered in Book Clubs at work this year. Dates are \<shrug\> because neither has actually been finished yet - the Domain-Driven Design book club seeme to peter out a few chapters from the end (though I'd already ducked out because of the bereavement), and Practical Process Automation is _technically_ still going though has stalled for the holiday season.
Regular readers will recall me reading [Domain-Driven Design]({{< ref "/posts/2023-wrap-up-books#domain-driven-design" >}}) last year, which fell a little flat as the concepts felt self-evident; this book's presentation was much more modern and useful. Though I still get a _real_ strong ick at the idea of Event Sourcing as a means of Domain Object Modelling - as with many things that That One Coworker™ suggests, the advantages seem miniscule and rare-in-practice, whereas the drawbacks and complexity are considerable.
As with "Thinking, Fast And Slow", taking notes as I went along was fruitful for these books.
# Full lists and stats
1. OverLondon
2. Use Of Weapons
3. Thinking, Fast And Slow
4. Curse Of Strahd
5. Trials Of Empire
6. All Systems Red
7. Artifical Condition
8. Rogue Protocol
9. Exit Strategy
10. Network Effect
11. Children Of Time
12. Children Of Ruin
13. Children Of Memory
14. Moving Pictures
15. The Eye Of The World
16. The Great Hunt
17. The Dragon Reborn
18. The Shadow Rising
19. The Fires Of Heaven
20. Lord Of Chaos
21. A Crown Of Sword
22. The Path Of Daggers
23. Wind And Truth // Learning Domain-Driven Design // Practical Process Automation (3 incomplete books roughly equals one complete book, right?)
* 21 Fiction, 4 Non-Fiction; counting CoS as non-fiction (debatable, for sure - but my heuristic is "_did I read this primarily in order to develop a skill, learn something, or obtain a new perspective, rather than for entertainment alone?_", by which it certainly qualifies) and counting both LD-DD and PPA.
* 19 by Men, 6 by Women, and 1 (Curse Of Strahd) by a group of authors including "_the husband and wife team of Tracy and Laura Hickman_".
* No books by non-white authors, as far as I'm aware
* 1 by Friends-or-Family
# Summing up, and looking forward
## Goals
Going into this year, I'd had the intention to read more non-fiction and more books by non-white/non-men. I had limited success there (slightly worse on non-fiction, quite a bit better on gender balance, full failure racial balance) - but to be honest I am cutting myself some significant slack given the bereavement that I suffered halfway through the year.
All of these goals remain solid ones for next year - I'd like to follow the advice of an excellent ex-mentor to alternate between fiction and non-fiction, and as a stretch goal would like to run a Book Club (though at present I'm not sure what I'd want to cover).
## General quality
Comparing with last year's reads, this year's falls distinctly short. Of the new books, only Children Of Time, Thinking, Fast And Slow, and Wind And Truth feel up to the average quality of last year's. Maybe the lesson here is to be faster to give up on books that aren't delighting me - though, in my defence, I was being _intentionally_ obstinate with WoT because completionism (through to a promised high-quality finale) was a deliberate goal. But also - sometimes you just win some or lose some.
## Articles
Last year I also included a [round-up of the significant articles that I read that year]({{< ref "/posts/2023-wrap-up-articles" >}}). Candidly I think it's pretty unlikely that I'll do that this year, as I never got around to installing [Linkwarden](https://linkwarden.app/) on my homeserver, and the stop-gap ([Pocket](https://getpocket.com)) contains a _whole_ bunch of 404s, empty content, and - bafflingly - links to things that seem "up my street" but that I have no memory of reading.
Do yourself a favour and simply read one of these all-time greats, instead:
* [The Categories Were Made For Man, Not Man For The Categories](https://slatestarcodex.com/2014/11/21/the-categories-were-made-for-man-not-man-for-the-categories/)
* [Many More Much Smaller Steps](https://www.geepawhill.org/2021/09/29/many-more-much-smaller-steps-first-sketch/) if you are a craftsperson of any form (though it's primarily written for software engineers)
* [Manual Work Is A Bug](https://queue.acm.org/detail.cfm?id=3197520) if you are a Software Engineer
[^friends]: among which I count myself not _just_ because of my friendship with noted word-wranglers [George](https://www.georgelockett.com/) and [Olivia](https://www.oliviawood.co.uk/), Failbetterers both at various times.
[^notes]: not counting textbooks at school - though, even then, the topics I studied meant that this was rarely relevant anyway. I still remember the horror on my History-student friend's face when I told him that, in the course of my three-year Maths degree, I had checked out a single book from the library, for the purposes of reading a single paragraph.
[^reliability-of-memory]: of course, the author would have something to say about the reliability of memory, here...
[^something-positive]: deliberately not linked because...no. JFC it's still going - that's a blast-from-the-past.
[^wheel-of-time-plot]: "_Rand moves into a new city; there is hostility and tension from the city's existing rulers; one of those rulers is exposed as a Foresaken; Rand unlocks a new power-up and defeats the Foresaken_". Not entirely fair; but far from false, either!
[^original-name]: the original name of the trope, which is now used for the [parent category](https://tvtropes.org/pmwiki/pmwiki.php/Main/SeinfeldIsUnfunny) instead.

View File

@ -1,7 +0,0 @@
---
title: "Adding RSS"
date: 2024-01-15T13:31:03-08:00
tags:
- meta
---
Inspired by [this article](https://rknight.me/blog/the-web-is-fantastic/), I've added (or attempted to?) an RSS feed to this blog. From [Hugo's docs](https://gohugo.io/templates/rss/) it _seems_ pretty simple, but please let me know if you run into any issues!

View File

@ -1,76 +0,0 @@
---
title: "Almost All Numbers Are Normal"
date: 2023-12-17T17:23:09+00:00
math: true
tags:
- mathematics
extraHeadContent:
- <link rel="stylesheet" type="text/css" href="/css/table-styling-almost-all-numbers.css">
---
"Almost All Numbers Are Normal" is a delightful sentence. In just five words, it relates three mathematical concepts, in a way which is true but misleading - the meaning of the sentence is almost exactly the opposite of what a layman would expect.
<!--more-->
## Numbers
The intuitive conception of "_numbers_" if you ask someone to simply "_name a number_" are the natural numbers $\mathbb{N}$ (0[^is-zero-a-natural-number], 1, 2, 3, ...), or the integers $\mathbb {Z}$ (... -3, -2, -1, 0, 1, 2, 3, ...). [Of course](https://xkcd.com/2501/) most folks are familiar with the rationals $\mathbb{Q}$, though probably by the name of and through the lens of "fractions" rather than the more mathematically-precise objects - and even those only scratch the surface of the full set of [real numbers](https://en.wikipedia.org/wiki/Real_number) $\mathbb{R}$ [and beyond](https://en.wikipedia.org/wiki/Complex_number#Generalizations_and_related_notions).
There are plenty of ways to conceptualize some of these sets of numbers - typically as the unique (up to isomorphism) structure satisfying some particular set of axioms like [Peano's](https://en.wikipedia.org/wiki/Peano_axioms#Set-theoretic_models) or [Dedekind's](https://en.wikipedia.org/wiki/Dedekind_cut) - but for the purposes of this post, I want to consider the reals[^limitation-of-consideration] as an infinite sequence[^what-about-the-decimal-point] of digits 0-9, or equivalently as a function $f: \mathbb{N} \to {0, 1, 2, \cdots 9}$. That is, the number `7394.23` is equivalent to the function partially represented by the following table:
| Index | Value |
|--------------|-----------|
| 1 | 7 |
| 2 | 3 |
| 3 | 9 |
| 4 | 4 |
| 5 | 2 |
| 6 | 3 |
| 7 | 0 |
| 8 | 0 |
| 9 | 0 |
| ... | ... |
I say _partially_ represented, because of course this table could continue infinitely - for any index greater than 6, the function's value is 0: [$\forall n > 6, f(n) = 0$].
This way of describing numbers focuses less on their value, and more on their written representation - it stresses the ability to ask "_what is the fifth digit of this number?_" much more than the ability to ask "_which of these two numbers is bigger?_". This focus will be justified in the next section.
## Normality
The word "normal" has lots of domain-specific meanings in mathematics, many of them related to one of two concepts:
* **orthogonality** - that's fancy mathematician speak for "_being at 90-degrees to something_"[^orthogonal]. For instance, we could say that a skyscraper is orthogonal to, or normal to, the ground, because it points straight upwards and the ground is horizontal.
* of or related to the **norm**, which itself is a function that assigns a length-like value to mathematical objects.
In particular - I don't think I've _ever_ heard the term "_normal_" used in its layman's sense of "_standard, expected, regular, average_"[^term-of-art]. I guess mathematicians don't think it's very normal to be normal.
In number theoretic terms, a [normal number](https://en.wikipedia.org/wiki/Normal_number)[^absolutely-normal] is one in which all digits and sequences of digits occur with the same frequency - no digit or sequence is "favoured". The string of digits looks like it could have been the output of a random number like coin-flipping (for binary digits) or repeatedly rolling a d10.
It's pretty easy to immediately see that no number with terminating decimal expansion (which includes all the integers, and all fractions with a denominator of a power of 10) are not normal - if the sequence of digits starts repeating 0, then 0 is "favoured", and the number is not normal. A little more thought shows that every rational number (every fraction) is abnormal - either the division terminates (and the decimal expansion continues `000...`), or the decimal expansion repeats (and so the repeated-string is "favoured", and any string which didn't appear before point that is absent).
### Corrolary of normalcy
A fun property of normal numbers is that, because all subsequences are "_equally likely_", and because they are infinite non-repeating sequences, any given sequence of numbers _must_ exist somewhere in them. Since any content that is stored on a computer is stored as a sequence of numbers, this implies that any content you could imagine - your name and birthday, the Director's Cut of Lord Of The Rings, a sequence of statements which prove that almost all numbers are normal - exists somewhere within each of them.
The trick would be _finding_ it...
## Almost All
Along with "normal", this is a common term which has a specified mathematical meaning - although, in this case, the meaning _is_ intuitive[^normal-meaning], just formally-defined.
A property is said to hold for "_almost all_" elements of a set if the complementary subset of elements for which the property does _not_ hold is negligible. The definition of negligible depends on the context, but will typically mean:
* A finite set inside an infinite set ("_almost all natural numbers are bigger than 10_" - because the set of numbers smaller-than-or-equal-to 10 is finite, and the set of naturals is infinite)
* A [countable](https://en.wikipedia.org/wiki/Countable_set) set inside an [uncountable](https://en.wikipedia.org/wiki/Uncountable_set) one, or generally a "smaller" infinity inside a bigger one.
This is probably the least surprising of the three concepts, but it does take a while for Maths undergrads to get their head round the co-feasibility of the statements "_P(x) is true for almost all x in S_" and "_P(x) is false for infinite x in S_".
## Putting it all together
So, putting it all together - "_almost all numbers are normal_" could be roughly translated as "_when considering the set of functions which map from $\mathbb{N}$ to ${0, 1, 2, ... 9}$, a negligible set of those functions result in sequences which have subsequences roughly evenly distributed_". Which is about as far as you could get from the results you'd get if you asked a layman to name some normal numbers - small natural numbers!
(I'm not actually going to present a proof of that fact here - I vaguely recall the shape of it, but being over a decade out of study, it's a little beyond my capability to present understandably. There are some reasonably accessible proofs [here](https://arxiv.org/pdf/2102.00493.pdf) and [here](https://www.colorado.edu/amath/sites/default/files/attached-files/math21-8.pdf) if you're interested!)
[^is-zero-a-natural-number]: If you have strong opinions on whether 0 is a natural number, you probably already know the rest of what I'm going to cover in this post.
[^limitation-of-consideration]: I don't think it's a cheat to limit my consideration to normal numbers here, since the concept of normality only applies to normal numbers. For any non-real number, the answer to "_is this normal?_" is `null`, `undefined`, or "_[mu](https://en.wikipedia.org/wiki/Mu_(negative)#Non-dualistic_meaning)_".
[^what-about-the-decimal-point]: For reasons that will become clear as I go on to talk about normality, we're ignoring the decimal point. That is, $123 \equiv 1.23 \equiv 0.000123$ for this discussion. Just trust me.
[^orthogonal]: Again - if you know enough to know why this statement is incorrect, you also know enough to know why I'm glossing over the complications.
[^term-of-art]: yes, I did intentionally pick words here which all have their own mathematical definitions. Language is fun!
[^absolutely-normal]: I'm only discussing base-10 here. A number which is normal in all integer bases >= 2 bears the wonderful label "_absolutely normal_".
[^normal-meaning]: that is - it has the normal meaning 😉

View File

@ -1,15 +0,0 @@
---
title: "Attribution on Mastodon"
date: 2024-10-14T09:44:25-07:00
tags:
- mastodon
- meta
---
Just a quick one to note that, following instructions on [this article](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/), I've added a meta tag to posts from this blog (`<meta name="fediverse:creator" content="scubbo@fosstodon.org" />`[^leading-at]) which should, hopefully, result in attribution when articles are shared on Mastodon (and apparently Discord too 🤷🏻‍♂️).
<!--more-->
At the time of writing (2024-10-21), it's still not working - I've asked on Mastodon [looking](https://fosstodon.org/@scubbo/113307063383002019) for some [pointers](https://fosstodon.org/@scubbo/113306849504125879), as well as examining [the original post]() and [another post that I know they're working well for](https://nex-3.com/blog/a-non-technical-intro-to-webmentions/) and [using a debugger tool](https://www.ogtester.app/og?url=https%3A%2F%2Fblog.scubbo.org%2Fposts%2Fattribution-on-mastodon%2F), but no luck so far...🤞🏻
[^leading-at]: note that, interestingly, the tag in the actual HTML does not include a leading `@`

View File

@ -1,89 +0,0 @@
---
title: "Auto Announce on Mastodon"
date: 2024-06-07T13:46:00+00:00
tags:
- CI/CD
- homelab
- meta
- vault
---
I just set up a step in my publication pipeline to automatically post on Mastodon when I publish a new blog post.
<!--more-->
The Mastodon API for posting is pretty easy, per [this guide](https://dev.to/bitsrfr/getting-started-with-the-mastodon-api-41jj). I grabbed a Mastodon token and put it into Vault to be accesible (as detailed in [previous]({{< ref "/posts/vault-secrets-into-k8s" >}}) [posts]({{< ref "/posts/base-app-infrastructure" >}})) to the CD pipeline.
Accessing Kubernetes secrets in Drone pipelines required installing the [Kubernetes Secrets Extension](https://docs.drone.io/runner/extensions/kube/)[^can-access-directly], which was [fairly easy](https://gitea.scubbo.org/scubbo/helm-charts/commit/8d70bbe78b1e818906a43913f489c120446c2276)[^sketchy-documentation]. I [already had Vault->Kubernetes Secret integration set up](https://gitea.scubbo.org/scubbo/helm-charts/commit/4c82c014f83020bad95cb81bc34767fef2c232c1), so plumbing the secret in was [also easy](https://gitea.scubbo.org/scubbo/helm-charts/commit/4cc1c531e270e6fbfd2af0219a0bf2eaa799a75c). I did run into a bit of confusion in that there's no mention in Drone docs of how to specify a non-`default` namespace from which to fetch Kubernetes secrets - turns out there are [two](https://github.com/drone/charts/blob/master/charts/drone-kubernetes-secrets/values.yaml#L74) [places](https://github.com/drone/charts/blob/master/charts/drone-kubernetes-secrets/values.yaml#L93) that need to be set to that value, not just one.
With all the pieces assembled, I just needed to write some hacky Bash (is there any other kind!?) to check for commits that are creating a new file in the `blog/content/posts/` directory (I don't want to "announce" commits that are only updates, or that are changing the system rather than the content), and let 'er rip. As a next step, it would be nice to extract this logic to a standalone image[^auto-update] for others to use as a Drone plugin - though my use-case is probably specific enough that this wouldn't be valuable. If you'd like it, though, let me know!
(I'm still not 100% sure that my [step to clear the cache on my main blog page](https://gitea.scubbo.org/scubbo/blogcontent/src/commit/58db334e96444d8768abe62bf42256a5b722efdc/.drone.yml#L117-L123) is working as expected - if you see unexpected behaviour that could be due to old cached values, please let me know!)
# My evolving views on CI/CD
During the process of implementing this change, I came to some realizations about the design and motivation of CI/CD systems.
I'm [pretty strongly on-the-record]({{< ref "/posts/ci-cd-cd, oh my" >}}) as believing that Amazon's internal Pipelines tool is one of the best things about developing there, and my opinion on that has only grown stronger with time (especially if you extend it to the dependency-version-management system, which almost entirely negates [Dependency Hell](https://en.wikipedia.org/wiki/Dependency_hell)). I was pretty surprised to get some [pushback](https://x.com/ZiggyTheHamster/status/1577076232243380230) on that from an ex-colleague when I first stated that position - he was extremely dissatisfied with Pipelines, in ways that made no sense to me. Nearly two years later, I think I've reached some better understanding - and, unsurprisingly, the disagreement seems to come from valuing different criteria and trying to do different things.
## Tasks to be completed by a pipeline
Amazon's Pipelines system is designed to publish libraries and deploy web services, and is extremely good at those tasks. That's all, though. If any post-build stages in your pipeline are doing anything other than "_take a built-image and update a running version of a software system to use that image, then run any associated tests and block the pipeline if appropriate_", You Are Going To Have A Bad Time. For instance, if you want a stage to run arbitrary bash code, or to make an HTTP request, or upload a generated artifact to S3, or whatever, you _can_ do those things (usually by hacking them in as functionality executed by a "Test" after a stage, or by setting up an external listener for SNS notifications), but you'll be fighting friction along the way. Developers who wish to do those things may, therefore, conclude that Pipelines is a bad tool; but they'd be wrong to do so. Pipelines is a excellent tool _for what it aims to do_, which is purely and simply "_publish a new version of a library_" and/or "_update the running versions of a web service_". It may be a bad tool _for your use-case_, but that doesn't make it a bad tool[^seven-languages].
The first obvious question then becomes - how often do developers want to do something outside of those two tasks? And I _think_ the answer is...almost never? I guess deployment of mobile apps would be one example (publishing an `.apk` probably looks different than building and publishing a Java/Python/etc. library, especially since you would need to run tests on deployed hardward before finalizing the publication), but I'm not really aware of any other examples. Please let me know if I'm missing something!
It is important to acknowledge, though, that Pipelines is _only_ a good solution in a corporate culture where there is one-and-only-one tool for the various SDLC functions (build, image storage, deployment, testing, etc.). With that level of standardization, your CD system can essentially accept configuration rather than code - tell it the names of your packages, the number of stages you want, etc., and it can spin up a personalized instance of The One Solution from your configuration parameters and a template. In the OSS world where the freedom to plug in different providers (GitHub/Gitea/GitLab, CircleCI/Drone/Woodpecker/Jenkins, Argo/Flux, etc.) is important, that "templatization" is nigh-impossible, and your pipeline definition has to be logic, not just parameters. That is - while I do miss Pipelines, I recognize it couldn't fly in the outside world.
## Different conceptualizations of a pipeline in OSS vs. Amazon
Unsurprisingly, I came out of Amazon with an Amazonian view of the relationship between a pipeline and a service - that is:
1. They're one-to-one - if you own and operate FooService, there is a single FooServicePipeline which manages build and deployment for the service (with pipeline stages corresponding to the various deployments of the service), rather than separate pipelines for build and for each deployment.
2. Multiple "waves" or executions can be progressing through a pipeline at a given time. The pipeline should take responsbility for ensuring that promotion into a stage does not start until the previous promotion has completed.
3. The version which is deployed to each stage is an emergent property of the executed logic of the pipeline, rather than being an input _to_ the pipeline.
After nearly two years of experience with OSS offerings, my views are a bit more nuanced and broad.
### One Pipeline per Service
I still tend to think that this is the most intuitive design - though, in fairness, you can squint and make "_one pipeline with multiple stages_" and "_one pipeline which orchestrates multiple sub-pipelines (which are each responsible for a stage of the overall-pipeline)_" look the same. This is really a question of definition - if you define "_pipeline_" as "_the thing which handles build and deployment for all stages of the service_", then by-definition there's only a single one. If you define "_pipeline_" as "_a sequence of steps to be executed_", then it's reasonable to say "_there's a pipeline for deploying to QA stage, there's a pipeline for deploying to Prod stage, and there's a pipeline for triggering those one-by-one_".
The important part, though, is the ability to set up dependencies and relations _between_ the stages - "_whatever was just deployed to QA stage (and passed tests) should now be promoted to Prod stage_". Thus, if you take the "_one pipeline per stage_" approach, there _must_ be an "over-pipeline" to make a change from one to the other; even if it doesn't exist as a standalone entity, and only exists implicitly in conditional logic determining whether completion of one step will trigger another. Which brings us to...
### Promotion Isolation
**Problem:** One thing (of many!) that Amazon Pipelines did _really well_ was to keep promotions isolated from one another. If a promotion is underway to Stage N and a promotion just succeeded to Stage N-1, the incoming promotion won't trigger until the ongoing one has completed. This is naturally desirable - each promotion should acquire a "lock" on a stage that is not released until the change being promoted has been tested.
**Solution:** This is possible to replicate in some OSS systems by limiting max-concurrency of executions to 1. If using the single overall pipeline model, this is a somewhat-extreme option which reduces throughput - if two changes are submitted in quick succession, the second change cannot begin flowing through the pipeline until the first one has completed (or failed along the way), so of _course_ there can be no race condition for a single stage - so, OSS systems probably work better with a model where each stage's deployment, and each stage-to-stage promotion, is a standalone pipeline.
**Problem:** Depending on team preference, it was also common at Amazon to configure the pipeline so that promotion into any non-prod stage will require manual approval if the previous promotion failed. This is so that a failed deployment can be preserved for debugging, rather than being overwitten by the next change that comes down the pipeline, which will obfuscate the original error.
**Solution:** I still haven't found a good way to implement this with OSS systems - this level of intra-step awareness seems to not be a common requirement of the systems. A deployment step(/pipeline) seems to only "_know about_" its own context, not the history of the "_next_" stage - indeed, the very notion of sequential stages does not seem to be "_baked in_" to these systems (see the earlier point that OSS pipeline systems are intentionally more abstract to support more supporting services, at the cost of having fewer high-level concepts available). One could of course implement this awareness, by extending the deployment step with API calls to the pipeline system which identify the next step and retrieve its history - but that's home-rolled implementation of functionality that I'm used to having provided "for free".
### The pipeline is a software system which decides what should be deployed where, rather than a system which is told what to deploy
This is the big conceptual change that took me a long time to adjust to in OSS systems. In my opinion, the input parameter to a(n overall) deployment pipeline should be "_the set of software packages to watch, build, and deploy_", **not** "_which built-versions should be deployed where_". The pipeline _itself_ is the software system responsible for triggering a deployment, running tests against the deployment, and determining whether the tests passed - and so, you should be _asking_ the pipeline "_is it correct to deploy image `deadbeef` to stage N+1?_", not _telling_ it to do so. Directly "injecting" stage/version mappings into the pipeline (by writing to the Deployment Repo) breaks sequentiality - it becomes possible to deploy something to stage N+1 other than the version which was just deployed to stage N, thus obviating all the benefits of sequential deployment and testing[^emergency-deployments].
Having stewed on this a little, and with the benefits of writing this post to disambiguate the differing views of pipelines, I think I've reached a good solution, though:
* Structure the deployment logic (in whatever automation system runs your pipeline) as taking a version and a stage as parameters.
* Write deployment logic which updates the Deployment Repo with the appropriate version, _and lock down permissions on the Deployment Repo so that only these automations can change it_ (though, see the last paragraph of this section)
* Either through an over-pipeline, or by adding a direct API call to the automation system at the end of a successful deployment, have the successful completion of "_deploy version X to stage N_" trigger a deployment of version X to stage N+1 (optionally, _if_ stage N+1's previous deployment was successful).
That way, the Deployment Repo becomes "internalized into" the pipeline - instead of the Repo being the interaction point at which the developers _tell_ the pipeline what to deploy, it instead becomes a state-maintainance persistence store where the pipeline _keeps track of_ what is currently deployed where (and to which external systems like Argo can couple/listen, to do their logic).
There's still an issue here, because developers _need_ to have write-access to the Deployment Repo of a service in order to be able to manage infrastructure of the service. Since this repo is where the mapping of "_which images should be deployed to which stage_" is found, developers _have_ the ability to override the pipeline. That's a solvable problem, though - automated checks during PRs can ensure that only those with appropriate permissions make changes in this mapping, while allowing any developer to manage general infrastructure. Again - something to roll for oneself, rather than having the functionality provided by the system, but that's the trade-off you make for general-purpose functionality.
[^can-access-directly]: Note that Drone actually has [the capability to access Vault secrets directly](https://docs.drone.io/secret/external/vault/) without importing them into Kubernetes, but I'd rather stick with the established access method that I already know about unless there are use-cases for which it doesn't work. In particular, note that the Kubernetes Secrets Extension has an [available Helm chart](https://github.com/drone/charts/tree/master/charts/drone-kubernetes-secrets) but the Vault Secrets Extension [doesn't](https://github.com/drone/charts/tree/master/charts)
[^sketchy-documentation]: Though do note the slightly misleading documentation - the docs instruct you to "_\[d\]eploy the secret extension in the same Pod as your Kubernetes runner._", but it seems that this is unnecessary and the extension can be deployed as a standalone pod (indeed, there's a [whole standalone Helm chart for it](https://github.com/drone/charts/tree/master/charts/drone-kubernetes-secrets)). In addition, the first paragraph in the [Kubernetes Secret](https://docs.drone.io/secret/external/kubernetes/) and [Kubernetes Secret Extension](https://docs.drone.io/runner/extensions/kube/) seem to be copy-pasted, and it's confusing that the [`extensions/secrets`](https://docs.drone.io/extensions/secret/) docs page links directly to the code of the Kubernetes Secret Extension rather than to its [docs page](https://docs.drone.io/runner/extensions/kube/). I've had a really tough time with Drone's documentation - in particular, there are plenty of contradictory indications of whether the Kubernetes runner is [deprecated](https://github.com/drone/charts/tree/master/charts/drone-runner-kube) or [not](https://docs.drone.io/pipeline/kubernetes/overview/). Functionality is also middling - I still can't believe that building with `buildx` requires [a plugin](https://github.com/thegeeklab/drone-docker-buildx)! If someone were starting afresh with CI, I'd definitely advise them to check another solution - current top of my list to checkout is [Woodpecker](https://woodpecker-ci.org), or [CircleCI](https://circleci.com/) seems to be the industry standard.
[^auto-update]: As I'd done with the [auto-repo-update-drone-plugin](https://gitea.scubbo.org/scubbo/auto-repo-update-drone-plugin) code, which automatically updates an Deployment Repo with the appropriate image - though I've since learned that this is better done through [`kustomize edit set image`](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/image.md).
[^seven-languages]: Shout-out to the excellent [Seven Langauges In Seven Weeks](https://www.amazon.com/Seven-Languages-Weeks-Programming-Programmers/dp/193435659X) book for introducing me to the notion that good design can include making it intentionally hard to do things with a particular tool, if that makes it easier/safer/faster to do the things you _want_ that tool to focus on. The Rust community take this idea to the extreme!
[^emergency-deployments]: Yes, breaking sequentiality is important functionality to make a direct (pipeline-circumventing) deployment in an emergency when you have an incident that can't be rolled back (say, due to non-backwards-compatible persistence changes) - but those should be extremely rare occurrences, not something to build your system around. Escalation to a dangerous-but-powerful privilege level should be extraordinary, not ordinary.
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,33 +0,0 @@
---
title: "Automatic Merging"
date: 2024-02-14T22:46:08-08:00
tags:
- CI/CD
- homelab
- productivity
- SDLC
---
When working on my personal projects, I typically just push straight to `main` - opening a PR just to approve it seems entirely pointless, as if I had been able to find any issues in my own work, I wouldn't wait to do it in a PR! However, this does mean that, if I forget to run any quality checkers (linters, tests, etc.), I won't find out about it until `on: push` GitHub Action runs, and even then I might not see the failure until several commits later.
<!--more-->
This problem _can_ be addressed with [pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks)[^spoiler], but I've never been a fan of them:
* As the documentation states, "_client-side hooks are not copied when you clone a repository_", meaning that any new collaborators (or even "_me after [blowing away the repo](https://xkcd.com/1597/)_") will silently miss out on them.
* On-commit seems like the wrong cadence to run quality checks - local commits should be fast and frequent checkpoints that you can return to (or juggle around) as needed, and adding friction to them makes development more precarious. Ideally, quality checks would run immediately prior to _pushing_, but the `pre-push` hook runs "_**after** the remote refs have been updated_", meaning[^do-i-understand-git] that even if they fail, the remote will still have been changed.
The other day I hit on a cool idea that would seem to address both problems - since GitHub allows PRs to be set to be automatically merged when all checks pass, perhaps I could set up a workflow whereby:
* I push commits to a `dev` branch
* An auto-merging PR is automatically opened _from_ that branch to `main`
* If the checks pass, the PR is automatically merged
* If it fails...well, I'd have to set up some non-email channel to notify myself about that, but that shouldn't be too hard
I did make [some progress on it](https://github.com/scubbo/edh-elo/tree/autoMergePR/.github/workflows), but ran into some issues:
* PRs cannot be created _in_ an AutoMerge state - they have to be set _into_ that state after creation. Although [this SO answer](https://stackoverflow.com/a/72259998/1040915) did describe how to do so, some quirk of GHA meant that that failed when executed _in_ a GHA context (claiming the PAT did not have permissions)
* All is well and good if the PR immediately passes - but if it fails and I make correcting commits onto `dev` (which update the PR), then when the PR passes and is squashed into a single commit to then be merged into `main`[^squash-and-merge], then `dev` and `main` will have diverged, and the next PR that's submitted from `dev` to `main` will appear to be contributing the preceding commits as well. Not ideal!
After a couple of hours of fiddling around, I returned to investigating `pre-commit` hooks, and found the [pre-commit](https://pre-commit.com/) _tool_, which provides a management interface for hooks. It unfortunately still requires manual installation (so a new contributor might not benefit from it - though, in fairness, that can be double-checked with CI checks), but the experience is smoother than writing hooks myself. I'll keep experimenting with it and see how I like it.
[^spoiler]: And - spoiler alert - after running into frustrations with my first approach, this was exactly what I ended up doing, using the [pre-commit](https://pre-commit.com/) tool.
[^do-i-understand-git]: I do admit I haven't actually tested this understanding. It does seem surprising, as it would make the `pre-push` hook basically useless. This also seems to contradict the documentation [here](https://github.com/git/git/blob/master/Documentation/RelNotes/1.8.2.txt) which states that "_"git push" will stop without doing anything if the new "pre-push" hook exists and exits with a failure._". So, maybe `pre-push` hooks _aren't_ useless? I've asked for more information on this [here](https://stackoverflow.com/questions/77998932/when-exactly-in-the-push-process-does-a-pre-push-hook-actually-run). But, the first counter-argument - and the convenience of the `pre-commit` _tool_ - have me still using `pre-commit` hooks, even if `pre-push` would have worked.
[^squash-and-merge]: I will die on the hill that "Squash And Merge" is the only sensible PR merge strategy. A Merge Commit means that you have non-linear history, and Rebase means that one _conceptual_ change is represented as however-many different commits were generated during development. There is no value whatsoever in preserving the frantic, scrabbling, experimental commits that were generated _during_ development - they are scaffolding that should be tidied away before presenting the finished product as a single commit![^irony]
[^irony]: Ironic, then, that in fact this tangled automation approach is one of the only cases where a Merge Commit would actually be...I can't believe I'm actually going to say this..._better_ 🤮

View File

@ -1,66 +0,0 @@
---
title: "Backups and Updates and Dependencies and Resiliency"
date: 2024-02-18T16:00:00-08:00
tags:
- homelab
- k8s
- SDLC
---
This post is going to be a bit of a meander. It starts with the description of a bug (and appropriate fix, in the hopes of [helping a fellow unfortunate](https://xkcd.com/979/)), continues on through a re-consideration of software engineering practice, and ends with a bit of pretentious terminological philosophy. Strap in, let's go!
<!--more-->
# The bug
I had a powercut at home recently, which wreaked a bit of havoc on my homelab - good reminder that I need to buy a UPS! Among other fun issues added to my Disaster Recovery backlog, I noticed that the [Sonarr](https://sonarr.tv/) container in my [Ombi](https://ombi.io/) pod was failing to start up, with logs that looked a little like[^not-actual-logs]:
```
[Fatal] ConsoleApp: EPIC FAIL!
[v4.0.0.615] NzbDrone.Common.Exceptions.SonarrStartupException: Sonarr failed to start: Error creating main database --->
System.Exception: constraint failed NOT NULL constraint failed: Commandstemp.QueuedAt While Processing: "INSERT INTO "Commands_temp" ("Id", "Name", "Body", "Priority", "Status", "QueuedAt", "StartedAt", "EndedAt", "Duration", "Exception", "Trigger", "Result") SELECT "Id", "Name", "Body", "Priority", "Status", "QueuedAt", "StartedAt", "EndedAt", "Duration", "Exception", "Trigger", "Result" FROM "Commands"" --->
code = Constraint (19), message = System.Data.SQLite.SQLiteException (0x800027AF): constraint failed NOT NULL
...
```
I could parse enough of this to know that something was wrong with the database, but not how to fix it.
After trying the standard approach of "_overwriting the database with a backup_[^backup]" - no dice - I went a-googling. It [seems](https://old.reddit.com/r/sonarr/comments/15p160j/v4_consoleapp_epic_fail_error/) that a buggy migration was introduced in `v4.0.0.614` of Sonarr, rendering startup impossible if there are any `Tasks` on the backlog in the database. Since my configuration [previously declared the image tag as simply `latest`](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/3dfc818f5f58e3a733fd7acd22269bf1ac94d21a/charts/ombi/templates/deployment.yaml#L57)[^watchtower], the pod restart triggered by the power outage pulled in the latest version, which included that buggy migration. Once I knew that, it was the work of several-but-not-too-many-moments to:
* `k scale deploy/ombi --replicas` to bring down the existing deployment (since I didn't want Sonarr itself messing with the database while I was editing it)
* Spin up a basic ops pod with the PVC attached - frustratingly there's [still no option to do so directly from `k run`](https://github.com/kubernetes/kubernetes/issues/30645), so I had to hand-craft a small Kubernetes manifest and `apply` it.
* Install `sqlite3` and blow away the `Tasks` table.
* Teardown my ops pod, rescale the Ombi pod, and confirm everything working as expected.
# The first realization - automatic dependency updates
This experience prompted me to re-evaluate how I think about updating dependencies[^what-are-dependencies]. Having only had professional Software Engineering experience at Amazon, a lot of my perspectives are naturally biased towards the Amazonian ways of doing things, and it's been an eye-opening experience to get more experience, contrast Amazon's processes with others', and see which I prefer[^ci-cd].
I'd always been a bit surprised to hear the advice to pin the _exact_ versions of your dependencies, and to only ever update them deliberately, not automatically. This, to me, seemed wasteful - if you trust your dependencies to [follow SemVer](https://semver.org/), you can safely naïvely pull in any non-major update, and know that you are:
* depending on the latest-and-greatest version of your dependency (complete with any efficiency gains, security patches, added functionality, etc.)
* never going to pull in anything that will break your system (because that, by definition, would be a Major SemVer change)
The key part of the preceding paragraph is "_if you trust your dependencies_". At Amazon, I did - any library I depended on was either explicitly written by a named team (whose office hours I could attend, whose Slack I could post in, whose Oncall I could pester), or was an external library deliberately ingested and maintained by the Third-Party Software Team. In both cases, I knew the folks responsible for ensuring the quality of the software available to me, and I knew that _they_ knew that they were accountable for it. I knew them to be held to (roughly!) the same standards that I was. Moreover, the sheer scale of the company meant that any issue in a library would be likely to be found, reported, investigated, and mitigated _even before my system did a regular daily scan for updates_. That is - the possible downside to me of automatically pulling in non-major changes was practically zero, so the benefit-ratio is nearly infinite. I can count on one hand the number of times that automatically pulling in updates caused any problems for me or my teams, and only one of those wasn't resolved by immediately taking an explicit dependency on the appropriate patch-version. Consequently, my services were set up to depend only on a specific Major Version of a library, and to automatically build against the most-recent Minor Version thereof.
But that's not the daily experience of developers, most of whom are taking dependencies mostly on external libraries, without the benefits of a 3P team vetting them for correctness, nor of accountability of the developing team to fix any reported issues immediately. In these situations - where there is non-negligible risk that a breaking change might be incorrectly published with a minor version update, or indeed that bugs might remain unreported or unfixed for long periods of time - it is prudent to pin an explicit version of each of your dependencies, and to only make any changes when there is a functionality, security, or other reason to update.
# The second realization - resiliency as inefficiency
Two phenomena described here -
* Having to buy a UPS, because PG&E can't be trusted to deliver reliable energy.
* Having to pin your dependency versions to not-the-latest-and-greatest minor-version, because their developers can't be trusted to deliver bug-free and correctly-SemVer'd updates.
...are examples of a broader phenomenon I've been noticing and seeking to name for some time - "_having to take proactive remediative/protective action because another party can't be trusted to measure up to reasonable expectations_". This is something that bugs me every time I notice it[^examples], because it is inefficient, _especially_ if the service-provider is providing a(n unreliable) service to many customers. At what point does the cost of thousands of UPSes outweigh the cost of, y'know, just providing reliable electricity[^complexity]?
In a showerthought this morning, I realized - _this is just [resiliency engineering](https://sre.google/sre-book/introduction/) in real life_. In fact, I remembered reading a quote from, I think, the much-fêted book "[How Infrastructure Works](https://www.amazon.com/How-Infrastructure-Works-Inside-Systems/dp/0593086597)", to the effect that any resiliency measure "_looks like_" inefficiency when judged solely on how well the system carries out its function _in the happy case_ - because the objective of resiliency is not to improve the behaviour of the happy case, but to make it more common by steering away from failure cases. Hopefully this change of perspective will allow me to meet these incidents with a little more equanimity in the future.
...and if you have any recommendations for a good UPS (ideally, but not necessarily, rack-mountable), please let me know!
[^not-actual-logs]: I didn't think to grab actual logs at the time - it was only in the shower a day or two later that I realized this provided the jumping-off point for this blog post. These logs are taken from [this Reddit post](https://old.reddit.com/r/sonarr/comments/15p160j/v4_consoleapp_epic_fail_error/), which I found invaluable in fixing the issue.
[^backup]: Handily, Sonarr seems to automatically create a `sonarr.db.BACKUP` file - at least, it was present and I didn't remember making it! 😝 but, even if that hadn't been the case, I [took my own advice]({{< ref "posts/check-your-backups" >}}) and set up backups with [BackBlaze](https://www.backblaze.com/), which _should_ have provided another avenues. That reminds me...the backup mechanism is overdue for a test...
[^watchtower]: I know, I know...installing [Watchtower](https://containrrr.dev/watchtower/) is on my list, I swear!
[^what-are-dependencies]: in this section I'm using "dependencies" to refer to "_software libraries used by the services that I as a professional software engineer own-and-operate_", but most of the same thinking applies to "_image tags of services that I deploy alongside my application that are owned and developed by people other than me or my team_".
[^ci-cd]: I will die on the hill that Amazon's internal [CI/CD system](https://blog.scubbo.org/posts/ci-cd-cd-oh-my/) is dramatically superior to any Open Source offering I've found, in ways that don't seem _that_ hard to replicate (primarily, though not solely, image specifications based on build metadata rather than hard-coded infra repo updates), and I'm frankly baffled as to why no-one's implementing their functionality?[^cunningham]
[^cunningham]: Yes, this _is_ a deliberate invocation of [Cunningham's Law](https://en.wikipedia.org/wiki/Ward_Cunningham#Law). _Please do_ prove me wrong!
[^examples]: Though, having _finally_ gotten around to blogging about it, I now can't bring to mind any of the examples that I'd noted.
[^complexity]: I'm glossing over a lot of complexity, here, and deliberately hand-waving away the fact that "_every problem looks easy from the outside_". It's perfectly possible that the difficulty of going from [5 9's](https://en.wikipedia.org/wiki/High_availability) of electrical uptime to 100% is impractical - that "_[the optimal amount of powercuts is non-zero](https://www.bitsaboutmoney.com/archive/optimal-amount-of-fraud/)_" - or that occasional powercuts aren't as impactful to the average consumer as they are homelab aficionadoes. Frankly, I doubt both points, given what I've heard about PG&E's business practices - but, nonetheless, the fact remains that every marginal improvement to a service-provider's service has a leveraged impact across all of its consumers. That break-even point might fall at different places, depending on the diminishing returns of improvement and on the number of customers - but the magnifying effect remains.

View File

@ -1,195 +0,0 @@
---
title: "Base App Infrastructure"
date: 2024-05-10T03:00:23-07:00
tags:
- crossplane
- homelab
- k8s
- SDLC
- vault
---
In my [previous post]({{< ref "/posts/vault-secrets-into-k8s" >}}), I had figured out how to inject Vault secrets into Kubernetes Secrets using the [Vault Secrets Operator](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator). My runthrough of the walkthrough worked, but I [swiftly ran into namespacing issues]({{< ref "/posts/vault-secrets-into-k8s#added-2024-04-29-namespacing-secrets" >}}) when trying to use it "_in production_".
<!--more-->
# The Problem
The setup can be divided into two parts[^platform-vs-app-team]:
* Creation of a Vault Role (with `boundServiceAccountNamespaces` corresponding with the k8s namespaces that should be permitted to access it) and Policy, and a k8s `VaultAuth` object telling the Vault Secrets Operator how to access the Vault Role.
* Creation of a `VaultStaticSecret` (referencing the VaultAuth object) in the app's `-deployment` repo, which results in a k8s secret.
As I started trying to extend my initial installation to other apps, I realized that simply adding more k8s namespaces to the `boundServiceAccountNamespaces` of a single Vault Role would not be a secure solution - it would allow _any_ pods in any of the bound namespaces to access any secret of any of the (other) applications. Ideally, each application-stage (or, equivalently, each k8s namespace[^namespaces-per-application]) would have its own resources created, with the Vault Role only accessible from that namespace[^sub-namespace-permissions].
## Why do I care?
You may be wondering why I care about Least Privilege - after all, it's only my own homelab, surely I know and trust every application that's running on it? Well, to an extent. I trust them enough to install them, but it still doesn't hurt to limit their privileges so that any unforeseen misbehaviour - whether deliberate or accidental - has limited impact. More importantly, my primary motivation in running this homelab is to learn and practice technical skills - the tasks don't have to be entirely practical, so long as they are educational! In fact, as you'll see shortly, this problem is almost-exactly equivalent to one I'm going to be solving at work soon, so doing this "right" is a good head-start.
# The solution
Ideally, I'd be able to automate (via extracted-and-parameterized logic) the creation of these resources as part of the application definition, since many apps will have similar requirements and I want to minimize any manual or imperative setup.
Thankfully, this is pretty close to a problem that I've been looking into at work, so I have a solution ready to go - [Crossplane](https://www.crossplane.io/), a tool that allows:
* management of "_External Resources_" (i.e. resources in systems outside Kubernetes, like Vault, Argo, etc.) via Kubernetes objects - i.e. you can declaratively create and update a Kubernetes object (a "_Managed Resource_") which represents the External Resource, and the Kubernetes reconciliation loop will keep the External Resource up-to-date.
* "bundling" of resources into Compositions - parameterized and inter-related collections of resources, analagous to Constructs in CDK.
![Diagram of the interrelation of the various Crossplane concepts](https://docs.crossplane.io/media/composition-how-it-works.svg "Diagram of the interrelation of the various Crossplane concepts")
With Crossplane in hand, the solution becomes simple:
* (while wearing my "Platform Team" hat) install a Provider (the interface between Crossplane and an external service) for Vault, and create a Composition which bundles the Vault resources that are necessary for Vault Secrets Operator setup.
* (wearing my "App team" hat) whenever I install an app which requires secret injection, do so alongside a Composite Resource (an instance of a Composition). All from the convenience of a single deployment repo, and with only a few extra lines of configuration!
## Walkthrough
You can see the solution [here](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml).
### XRD
(Lines 1-26) A [Composite Resource Definition](https://docs.crossplane.io/latest/concepts/composite-resource-definitions/) (or "XRD" - yeah, I know, but Kubernetes had already taken the term "CRD") is like (in Programming Language terms) the Interface to a [Composition](https://docs.crossplane.io/latest/concepts/compositions/)'s Implementation, or (in Web Service) the API Spec or schema. It defines how a consumer can invoke a Composition - the name they should use, and the parameters they should pass. Consumers can either invoke this by its name if creating a cluster-scoped [Composite Resource](https://docs.crossplane.io/latest/concepts/composite-resources/), or in a namespaced context via a [Claim](https://docs.crossplane.io/latest/concepts/claims/).
This definition is saying:
* (Lines 6-8) "_There's a Composition that can be addressed as `xbaseapplicationinfrastructures.scubbo.org`_..."
* (Lines 10-12) "_...(which can also be addressed by the Claim Name `BaseAppInfra`)..._"
* (Lines 13-25) "_...which has only a single version defined, which takes a single string parameter named `appName`_"
It is apparently possible to provide [multiple schema versions](https://docs.crossplane.io/v1.15/concepts/composite-resource-definitions/#multiple-schema-versions) - but since "_new required fields are a 'breaking change.'_" and "_Only one version can be `referenceable` \[...which...\] indicates which version of the schema Compositions use_", I'm not really sure how that is actually useful - and this is borne out by the fact that "_Crossplane recommends implementing breaking schema changes as brand new XRDs._".
### Top-level Composition
The only point to note in lines 29-36 is that `spec.compositeTypeRef.apiVersion` and `spec.compositeTypeRef.kind` must match the values set on 6, 8, and 14.
### Vault Resources
Lines 37-136 define Vault Resources, provided by the [Vault Provider](https://github.com/upbound/provider-vault). These create a Vault Role, Policy, and KV Secrets Mount roughly as described in the [walkthrough](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator). Note the use of [patches and transforms](https://docs.crossplane.io/latest/concepts/patch-and-transform/) to set values in the Managed Resources based on properties of the Claim (the Kubernetes namespace and the parameter `appName`)
### Kubernetes Resource
The [Vault Secrets Operator walkthrough](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator) also [requires](https://github.com/hashicorp-education/learn-vault-secrets-operator/blob/main/vault/vault-auth-static.yaml) the creation of a `VaultAuth` object (specifying how the Secrets Operator should authenticate to Vault - i.e. which Role to use), and that is [not an object provided by the Vault Provider](https://doc.crds.dev/github.com/upbound/provider-vault)[^limited-vault-provider], so I also needed to use the [Kubernetes Provider](https://github.com/crossplane-contrib/provider-kubernetes) to create an arbitrary Kubernetes object as part of the Composition.
### Actual usage
After deploying this Composition to my cluster, actual usage was a doddle:
```bash
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: example-namespace-for-crossplane-vault-secrets-demo
---
apiVersion: scubbo.org/v1alpha1
kind: BaseAppInfra
metadata:
name: example-app-base-infra
namespace: example-namespace-for-crossplane-vault-secrets-demo
spec:
appName: example-app
EOF
namespace/example-namespace-for-crossplane-vault-secrets-demo created
baseappinfra.scubbo.org/example-app-base-infra created
$ kubectl ns example-namespace-for-crossplane-vault-secrets-demo
Context "default" modified.
Active namespace is "example-namespace-for-crossplane-vault-secrets-demo".
$ kubectl get BaseAppInfra example-app-base-infra
NAME SYNCED READY CONNECTION-SECRET AGE
example-app-base-infra True True 29s
$ vault secrets list | grep 'example-app'
app-example-app-kv/ kv kv_d4b378a7 KV storage for app example-app
$ vault read auth/kubernetes/role/vault-secrets-operator-example-app-role
Key Value
--- -----
alias_name_source serviceaccount_uid
audience vault
bound_service_account_names [default]
bound_service_account_namespaces [example-namespace-for-crossplane-vault-secrets-demo]
token_bound_cidrs []
token_explicit_max_ttl 0s
token_max_ttl 0s
token_no_default_policy false
token_num_uses 0
token_period 0s
token_policies [vault-secrets-operator-example-app-policy]
token_ttl 24h
token_type default
$ vault kv put -mount app-example-app-kv example-secret key=value-but-make-it-secret
============= Secret Path =============
app-example-app-kv/data/example-secret
======= Metadata =======
Key Value
--- -----
created_time 2024-05-09T05:53:59.20680794Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
$ kubectl get secrets
No resources found in example-namespace-for-crossplane-vault-secrets-demo namespace.
$ cat <<EOF | kubectl apply -f -
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vault-kv-app
namespace: example-namespace-for-crossplane-vault-secrets-demo
spec:
type: kv-v2
mount: app-example-app-kv
path: example-secret
destination:
name: secretkv
create: true
refreshAfter: 30s
vaultAuthRef: vault-auth-example-app
EOF
vaultstaticsecret.secrets.hashicorp.com/vault-kv-app created
$ kubectl get VaultStaticSecret
NAME AGE
vault-kv-app 6s
$ kubectl get secrets
NAME TYPE DATA AGE
secretkv Opaque 2 23s
$ kubectl get secret secretkv -o jsonpath='{.data.key}' | base64 -d
value-but-make-it-secret
```
Almost all of the steps above were executed "as if" I was a memeber of the App Team. The Platform Team (or, more accurately, automation owned by the Platform Team, but triggered during Application Creation via the Developer Platform like [Backstage](https://backstage.io/)) should take care of creating the Namespace, but everything else - creating the `BaseAppInfra`, populating the Vault Secret, and creating the `VaultStaticSecret` - are tasks that the App Team can handle.
# Next Steps and Further Thoughts
* Unwinding my yak-shaving-stack by another level, my motivation for injecting secrets from Vault was to be able to set up [Velero](https://velero.io/) with AWS Credentials so I can back up my PVs to S3. Most of my pods are using my TrueNAS cluster as a persistent storage provider (thanks to [this great walkthrough](https://jonathangazeley.com/2021/01/05/using-truenas-to-provide-persistent-storage-for-kubernetes/)), with RAID for redundancy[^raidz1], so they should be _reasonably_ durable - but, backups are still important!
* I should probably export the ZFS Snapshots off-site as well. The task stack never ends...
* My system's getting complex enough that an architecture diagram in the [README](https://gitea.scubbo.org/scubbo/helm-charts) would be useful - at least, as a reminder to myself of what tools I have running, even if no-one else would be interested!
* Because I'm using an [App Of Apps Pattern](https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/), I don't need to create Argo Applications[^argo-application] and Policies for the applications - but, for setups that don't use that pattern (like, say, my work :P ), those resources should also be part of the Base Infrastructure. Assumable (Vault) Roles for the application _itself_ to use would also be good.
* This setup defines a Composition that any App Team can create (via Claim), but I haven't looked into how to prevent (non-admin) users from creating arbitrary Managed Resources (outside the scope of a Composition). That is, there's nothing to prevent a user from using Crossplane to create a Vault Policy that has global access, creating a Vault Role using that Policy that's available to their namespace, and wreaking havoc. I suspect this would be a use-case for [Kyverno](https://kyverno.io/), [OpenPolicyAgent](https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/), or other policy tools.
* Several fields in the [Composition](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml) are mutually-dependent. For instance, the name of the Vault Role ([line 71](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml#L71)) must be referenced by the VaultAuth on [line 166](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml#L166), and the name of the Vault Policy ([line 128](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml#L128)) must be assigned to the Role on [line 79](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/e798564692f71187e3ff3f9d77f3aa1c46ca9ee4/charts/vault-crossplane-integration/base-app-infra.yaml#L79). I'd _love_ to use [cdk8s](https://cdk8s.io/) to _define_ the resources instantiated by Crossplane, so that these dependencies can be made explicit, rather than incidental. As a coworker of mine is fond of proclaiming, "_YAML is the assembly language of Cloud-Native_" - although it's a universally-comprehended language that tools can use to communicate, we as human developers should be using higher-level tools and abstractions.
* I've still only used the Secrets Operator to inject static secrets. I'd be interested to see how [Dynamic Secrets](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator#setup-dynamic-secrets) - for secret values which have to change over time, such as TTL'd creds for other services - would work. According to the [docs](https://kubernetes.io/docs/concepts/configuration/secret/#editing-a-secret), "_updates to existing `Secret` objects are propagated automatically to Pods that use the data_", which is pretty cool.
* An alternative would be to use the [Vault Sidecar Injector service](https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-sidecar) to directly inject and update Vault secret values into the Pod. At first glance, I'd be averse to running both this _and_ Vault Secrets Operator - I'd prefer to have one-and-only-one way of getting Vault Secrets into Kubernetes, and VSO (plus native Secret mounting) seems to cover all use-cases whereas Vault Sidecare only covers injection (and not setting Secret values as env variables) - but, it's always good to know the alternative options!
[^platform-vs-app-team]: In a production setup in a fully-operationalized company, these tasks would be carried out by the Platform team and by the App team, respectively. Obviously in my own homelab setup, I fulfil both roles - but if it ever seems odd in this article that I'm jumping through hoops to keep permission segregated "from myself", keep in mind that I'm effectively "acting as" two different teams.
[^namespaces-per-application]: or, for grants that should be available to all stages of the application, "_each set of namespaces which correspond with a single application_". IDK if this is an industry-standard Best Practice, but the norm at work is to have k8s namespaces `foo-application-dev`, `foo-application-qa`, and `foo-application-prod` for each of the stages of the application, which seems like a sensible way to limit blast radius of changes. I wonder if there's a k8s-native concept of "namespace hierarchies", where you could define (say) a parent namespace `foo-application` (which "contains" the three leaf namespaces), and havy any permission grants "trickle down" to its children.
[^sub-namespace-permissions]: not relevant right now, but I wonder if there's a use-case for even stricter restrictions than just the namespace granularity. I can imagine a case where there are several pods/jobs within a(n application-stage - that is, within a) namespace, but where a given secret should only be accessible to a subset of them. Something for Future-Jack to look into! That level of restriction would presumably be handled at the k8s-level, not the Vault level - the App/Platform boundary interface ensures only that the right secrets are available to the right App, and then the App itself (via k8s) is responsible for further scope-restrictions.
[^limited-vault-provider]: which is fair - it's a CRD inherent to the Vault Secrets Operator and is an object which exists "in" Kubernetes, not in the external service Vault itself
[^raidz1]: Only RAIDZ1, which is apparently [frowned upon](https://serverfault.com/questions/634197/zfs-is-raidz-1-really-that-bad) - but, given that I'm paying for my own hardware rather than designing it for a corporate budget, I'm making a tradeoff between redundancy and cost-of-drives.
[^argo-application]: Argo is pretty great as a tool, but I will _never_ forgive them for the heinous naming decision of giving the name "Application" to "_a single stage/deployment of an application_"
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,26 +0,0 @@
---
title: "Books as Vehicles"
date: 2023-12-11T20:19:13-08:00
tags:
- snippets
---
"The Liar", Stephen Fry's first novel follows a Wildean young man studying language at Cambridge University. I wonder where he got his inspiration.
<!--more-->
This passage covers our hero Adrian's meeting with his Senior Tutor, philology professor Trefusis:
> Trefusis's quarters could be described in one word.
>
> Books.
>
> Books and books and books. And then, just when an observer might be lured into thinking that that must be it, more books.
>
> Barely a square inch of wood or wall or floor was visible. Walking was only allowed by pathways cut between the piles of books. Treading these pathways with books waist-high either side was like negotiating a maze. Trefusis called the room his 'librarinth'. Areas where seating was possible were like lagoons in a coral strand of books.
>
> Adrian supposed that any man who could speak twenty-three languages and read forty was likely to collect a few improving volumes along the way. Trefusis himself was highly dismissive of them.
>
> 'Waste of trees,' he had once said. 'Stupid, ugly, clumsy, heavy things. The sooner technology comes up with a reliable alternative the better.'
>
> Early in the term he had flung a book at Adrian's head in irritation at some crass comment. Adrian had caught it and been shocked to see that it was a first edition of _Les Fleurs de Mal_.
>
> 'Books are not holy relics,' Trefusis had said. 'Words may be my religion, but when it comes to worship, I am very low church. The temples and the graven images are of no interest to me. The superstitious mammetry of a bourgeois obsession for books is severely annoying. Think how many children are put off reading by prissy little people ticking them off whenever they turn a page carelessly. The world is so fond of saying that books should be "treated with respect". But when are we told that _words_ should be treated with respect? From our earliest years we are taught to revere only the outward and visible. Ghastly literary types maundering on about books as "objects". Yes, that does happen to be a first edition. A present from Noel Annan, as a matter of fact. But I assure you that a foul yellow _livre de poche_ would have been just as useful to me. Not that I fail to appreciate Noel's generosity. A book is a piece of technology. If people wish to amass them and pay high prices for this one or that, well and good. But they can't pretend that it is any higher or more intelligent a calling than collecting snuff-boxes or bubble-gum cards. I may read a book, I may use it as an ashtray, a paperweight, a doorstop or even as a missile to throw at silly young men who make fatuous remarks. So. Think again.' And Adrian had thought again.

View File

@ -5,7 +5,6 @@ tags:
- homelab
- CI/CD
- meta
- SDLC
---
Since leaving Amazon ~4 months ago and dedicating more time to my own personal projects (and actually trying to ship things instead of getting distracted a few days in by the next shiny project!), I've learned a lot more about the Open Source tools that are available to software engineers; which, in turn, has highlighted a few areas of ignorance about CI/CD Pipelines. Emulating [Julia Evans](https://jvns.ca/), I'm writing this blog both to help lead others who might have similar questions, and to [rubber-duck](https://en.wikipedia.org/wiki/Rubber_duck_debugging) my own process of answering the questions.

View File

@ -2,9 +2,7 @@
title: "Cloudflare Tunnel DNS"
date: 2022-08-22T16:05:39-07:00
tags:
- cloudflare-tunnels
- homelab
- k8s
- meta
---

View File

@ -1,11 +0,0 @@
---
title: "Conditional Cleanups in Pytest"
date: 2024-04-28T16:55:37-07:00
tags:
- python
- testing
---
A helpful pattern in testing is to take some cleanup action _only_ if the test passes/fails. For instance, for a test which interacts with an on-filesystem database, the database should be deleted if the test passes, but it should stick around if the test fails so that the developer can examine it and debug.
<!--more-->
In JUnit, this is possible [via a `@Rule`](http://www.thinkcode.se/blog/2012/07/08/performing-an-action-when-a-test-fails), but as far as I can tell there's no pre-built equivalent in Python's `pytest`. I did find [this StackOverflow answer](https://stackoverflow.com/a/69283090/1040915) describing an approach using the [`pytest_runtest_makereport`](https://docs.pytest.org/en/latest/reference/reference.html#pytest.hookspec.pytest_runtest_makereport) hook, though the syntax appears to have changed since that answer. I put together an example implementation [here](https://gitea.scubbo.org/scubbo/pytest-conditional-cleanup-demo), which also adds the ability for fixtures _and_ tests to add "cleanup" actions to a stack, which will be executed in reverse order.

View File

@ -3,9 +3,6 @@ title: "Edit Command Line in Zsh"
date: 2022-07-10T00:25:21-07:00
---
**EDIT 2024-04-16**: turns out that there's a [built-in](https://www.computerhope.com/unix/uhistory.htm), `fc`, which does basically the same thing, though it edits the command that was _just entered_ (which is typically what you want when you encounter an error or want to do "the next thing", anyway).
While reading through my dotfiles, I found some [configuration](https://github.com/scubbo/dotfiles/blob/690f907f9ae36e36fed9851eac3a4ff2c20d7905/zshrc-local-mactop#L144-L147)[^1] that didn't seem to be working - it claimed that `<ESC>,v` would allow editing of the current line in vim, but that didn't seem to work. I guess I'd copied that from some other configuration and lost patience with trying to get it working, or that it relied on some other configuration option which had been broken[^2]. I dug in to find out more. ([This article](https://thevaluable.dev/zsh-line-editor-configuration-mouseless/) was invaluable!)
<!--more-->
## Intention
@ -56,4 +53,4 @@ bindkey -M viins '^U' kill-whole-line
[^1]: Still in Github until I fully migrate to my self-hosted [Gitea](https://gitea.scubbo.org/) instance. I'm cautious of a circular dependency here - Gitea would need to be up-and-available to source dotfiles, but dotfiles would be referenced as part of the setup process for hosts (including the one that runs the Gitea instance).
[^2]: An idea - regression testing for dotfiles? Don't tempt me...
[^3]: The [article](https://thevaluable.dev/zsh-line-editor-configuration-mouseless/) says that the ZLE _is_ the command prompt, which...seems unlikely to me? I would think that the ZLE is a part _of_ the command prompt, but not all of it? Although the article contains a lot of useful information and insight, it also has some rather loose and imprecise statements, so I'm not sure how much to trust this.
[^4]: `bindkey -v` is an alias for `bindkey -A viins main` - in ZLE, you don't set a keymap as active, instead you set a keymap as an alias for `main`, and I think that's beautiful.
[^4]: `bindkey -v` is an alias for `bindkey -A viins main` - in ZLE, you don't set a keymap as active, instead you set a keymap as an alias for `main`, and I think that's beautiful.

View File

@ -1,52 +0,0 @@
---
title: "Excluding Weeknotes From Main Page"
date: 2025-04-06T21:18:49-07:00
tags:
- Meta
- Tech-Snippets
---
I just went to write up a new [weeknotes]({{< ref "/tags/weeknotes" >}}) post, and noticed that that would have meant that all three previewed posts on my main page would have been weeknotes. That simply will not do! So into the depths of Hugo layouts I ventured once more.
<!--more-->
The relevant part of the original layout looks like[^line-numbers] this:
```
...
{{ $section := where $.Site.RegularPages "Section" "in" $section_name }}
{{ $section_count := len $section }}
{{ if ge $section_count 1 }}
<div class="pa3 pa4-ns w-100 w-70-ns center">
{{/* Use $section_name to get the section title. Use "with" to only show it if it exists */}}
{{ with $.Site.GetPage "section" $section_name }}
<h1 class="flex-none">
{{ $.Param "recent_copy" | default (i18n "recentTitle" .) }}
</h1>
{{ end }}
{{ $n_posts := $.Param "recent_posts_number" | default 3 }}
<section class="w-100 mw8">
{{/* Range through the first $n_posts items of the section */}}
{{ range (first $n_posts $section) }}
<div class="relative w-100 mb4">
{{ .Render "summary-with-image" }}
</div>
{{ end }}
</section>
...
```
Although the [where function](https://gohugo.io/functions/collections/where) does have a pretty good selection of operators, there's no `not` or `not intersection` - so, although it's possible to [filter to all members which have a particular slice-term contained in some other slice](https://gohugo.io/functions/collections/where/#intersection-comparison), it's not immediately possible to find all members that _don't_ have a given value in a slice-term. Thankfully, [later in the same docs](https://gohugo.io/functions/collections/where/#inequality-test) there's a link to [`collections/complement`](https://gohugo.io/functions/collections/complement/), which does exactly what I want. The final result[^commit] was:
```
...
{{ $section_original := where $.Site.RegularPages "Section" "in" $section_name }}
{{ $weeknotes := where $section_original "Params.tags" "intersect" (slice "Weeknotes") }}
{{ $section := complement $weeknotes $section_original }}
...
```
Since I don't want those weeknotes to be undiscoverable, though, I also added a dedicated section for them on the homepage. Pretty happy with how that turned out!
[^line-numbers]: Hmm, note to self for a TODO - automatically adding line-numbers into monospace blocks would be nice!
[^commit]: See the actual commit [here](https://gitea.scubbo.org/scubbo/blogcontent/commit/2220b539ea9e6d61117bc72fe371b3544515e732)!

View File

@ -1,89 +0,0 @@
---
title: "Gitea Actions"
date: 2025-02-27T21:25:00-08:00
tags:
- CI/CD
- Gitea
- Homelab
- K8s
- Meta
---
As I hoped in my [last post]({{< ref "/posts/weeknotes-the-first" >}}), I've set up [Gitea Actions](https://docs.gitea.com/usage/actions/overview) on my homelab, with a view to completely replacing [Drone](https://docs.gitea.com/usage/actions/overview) which I've found to be pretty buggy and missing some core features[^ill-featured]. The process was _reasonably_ smooth, but not entirely turnkey, so I've laid out the steps I took in the hopes that they'll help someone else.
<!--more-->
I'm using the [Helm Chart](https://gitea.com/gitea/helm-chart/) to manage my Gitea installation - if you want to install Action Runners separately, there's a good guide [here](https://docs.gitea.com/usage/actions/quickstart).
# Step-by-step instructions
## Upgrade
First and foremost - Gitea Actions was introduced in Gitea 1.19, but in practice I [found](https://gitea.com/gitea/helm-chart/issues/813) that some of the feature of the Helm Chart required at least `1.24.0`[^patch-version]. You'll also need your Helm Chart to be on at least `10.6.0` - read the [upgrading guide](https://gitea.com/gitea/helm-chart/#upgrading) _carefully_, I nearly borked my whole installation by jumping versions too quickly. [Here's](https://github.com/scubbo/pi-tools/commit/2b72a106ec14f042428058b94a0b38f7f0bcc8f1) my upgrade.
## (Potentially) Patch the Helm Chart for persistence
Depending on how you provide PVs to your Pods, this might not affect you, but I and some others found a [bug](https://gitea.com/gitea/helm-chart/issues/764) whereby the (hard-coded) PV size for the Runners was too small to be provided by the provisioner (honestly didn't even know that was a constraint that existed!). This gave me a great opportunity to learn how to use a locally-developed Helm Chart (rather than a published one) as a dependency for your own, and it was super-easy, barely an inconvenience:
* Clone the [chart](https://gitea.com/gitea/helm-chart)
* Make whatever alterations you want ([e.g.](https://gitea.com/gitea/helm-chart/pulls/812))
* If the chart has dependencies (as Gitea's does), run `helm dependency update`
* Run `helm package .` from the root of the repo - this will create a file named `<name>-<version>.tgz`, where those variables are taken from the `Chart.yaml`
* Move that archive into `charts/` in your own chart's directory - so long as the version number matches the desired-version specified in your own `Chart.yaml`, it will be picked up over a remote version.
## Adapt your workflow to the absence of Node
At this point, you should be able to run a workflow (don't forget that you need to [enable repository actions](https://docs.gitea.com/usage/actions/quickstart#use-actions) on a repo-by-repo basis), but you might notice that `actions/checkout` fails with `'node' not found in $PATH`, even when following the [Quickstart example](https://docs.gitea.com/usage/actions/quickstart). Some lengthy investigation [here](https://gitea.com/gitea/act_runner/issues/538) highlights differences between Gitea Actions Runners and GitHub Actions Runners - TL;DR, you need to:
* Update `runner.labels` in the `gitea-act-runner-config` ConfigMap to include `"ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"` (as [here](https://github.com/scubbo/pi-tools/blob/main/k8s-objects/helm-charts/gitea/values.yaml#L93))
* Install Node _before_ using the `actions/checkout` action, as [here](https://gitea.scubbo.org/scubbo/blogcontent/src/commit/5176ec26ff679ee9ebde6467eefed8d8c39d775c/.gitea/workflows/publish.yaml#L9-L14).
If you're going to use Gitea Actions a lot, it's probably sensible for you to create your own base image with Node preinstalled.
## Plumbthrough Docker Socket
If you're going to be using Actions to build a Docker image, you'll need to make the Docker socket available to the Docker-in-Docker containers that run _within_ the `act_runner`. [This comment](https://gitea.com/gitea/act_runner/issues/280#issuecomment-898726) by `@javiertury` was very helpful, especially once I realized that setting `DOCKER_HOST` on the runner-pod is not the same thing as setting it on the containers that run _within_ that pod - you can see the full configuration I needed [here](https://github.com/scubbo/pi-tools/blob/main/k8s-objects/helm-charts/gitea/values.yaml#L94-L109).
An alternative approach using the `kubernetes` driver is detailed [here](https://tobru.ch/gitea-actions-container-builds/). I haven't tried it - I do like the idea of jobs being standalone Pods, with all the control that k8s can offer (with annotations, operators, etc.), but I didn't fancy having to provide a `kubeconfig` and specify `driver: kubernetes` on every build step. I suppose it would be possible to abstract away that configuration by creating my own build-action - [the solution to every problem...](https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering)
Although, on second thoughts - if _only_ docker-builds run as separate pods, not _every_ job/step in a workflow, then that a) leads to some inconsistency, and b) limits the value of the k8s-magic that can be injected. Maybe it's less attractive than I thought.
## Inject secrets
In order to push to the Gitea Docker Registry, or to auto-update a Deployment Repo, the workflow needs to authenticate to Gitea with a PAT. By this point in the night I was getting pretty tired and impatient, so I took the simple route of [creating a secret in the repo](https://docs.gitea.com/usage/actions/secrets) containing the PAT; but it should be possible to use the [vault-action](https://github.com/hashicorp/vault-action) (remember, Gitea Actions is compatible with published actions for GitHub Actions!) to retrieve a secret, once Vault's been set up to accept OIDC auth. Actually _generating_ such a secret might be tricky - I know there's a [plugin to provide ephemeral finely-scoped GitHub Tokens from Vault](https://github.com/martinbaillie/vault-plugin-secrets-github), but I doubt it would be directly installable on Gitea.
I've still yet to plumb in the Telegram token I used to send myself a confirmation message after publication - good opportunity for the Vault-based approach.
## Update to more modern infrastructure management
This isn't something that _you_ need to do, but rather something that I had the _opportunity_ to do and that I want to brag about.
I'd previously been managing the infrastructure of this blog[^infrastructure] with a [Helm chart](https://gitea.scubbo.org/scubbo/blog-infrastructure), as that was the only way I could figure out to [extract some configuration from a definition so that it could be easily updated](https://gitea.scubbo.org/scubbo/blog-infrastructure/src/branch/main/helm/templates/_helpers.tpl). Since I've discovered `kustomize` in the intervening time, no reason not to [use it](https://gitea.scubbo.org/scubbo/blogcontent/src/commit/5176ec26ff679ee9ebde6467eefed8d8c39d775c/.gitea/workflows/publish.yaml#L93), in a repo that [conforms to normal naming conventions](https://gitea.scubbo.org/scubbo/blog-deployment). I also took this opportunity to move the Argo App Definition from a [repetitive declaration in a long YAML file](https://gitea.scubbo.org/scubbo/helm-charts/commit/fb7e8cd98e37db111bed0bd3c983e2e0157b4be6#diff-9274f28f68613fb77d22af9241e5f859b4c035b8) to a [concise invocation](https://gitea.scubbo.org/scubbo/helm-charts/commit/fb7e8cd98e37db111bed0bd3c983e2e0157b4be6#diff-9aa4e4421d8484121d4344945de5fe2b6e99bf37) of an [extracted template](https://gitea.scubbo.org/scubbo/helm-charts/commit/fb7e8cd98e37db111bed0bd3c983e2e0157b4be6#diff-1b8af0da046a40253682f731d114cef2e87ea244).
## Next Steps
(Again - this is for me, not you!)
* I might extract the "_build, push, and update the deployment repo_" logic to a standalone workflow. I've already done this at work, and it was very helpful there. I have fewer use-cases and parameters to support for my own use!
* As detailed above, reimplementing a Telegram notification when this publishes would be neat, as would the [post to Mastodon step](https://gitea.scubbo.org/scubbo/blogcontent/src/commit/e125f5795e95bddfc108641507b79b0d8add45f2/.drone.yml#L107-L132).
* And injecting secrets via Vault
* I don't have any plans to add a call to Argo to sync the app after updating the Deployment Repo - Auto-Sync has always been good enough for me.
A much larger-scale change (which might end up obsoleting Gitea Actions altogether) would be to install [CodeFresh](https://codefresh.io/) or a similar pipeline visualization and management tool; not that that's really relevant for the teensy single-stage "pipeline" of this blog, but I could deploy test apps with it to experiment. I've been out of Amazon for over two years, and I'm still aghast at how awful the accepted state-of-the-art is for Pipeline management in OSS tooling (as I previously blogged [here]({{< ref "/posts/ci-cd-cd, oh my" >}})). There doesn't seem to be any tool that takes the view that the Pipeline is a first-class entity which _manages_ the deployments and promotions, as opposed to deployments being the top-level concepts that are triggered by each other or by _<shudder>_ pushes to "_environment branches_". The "Deployments" page for GitHub is the closest thing I've found to a visualization, but it doesn't show you the actual state _of_ the pipeline, provide any controls to it (setting/overriding deployment windows, manually rolling-back/promoting/force-passing), or provide any built-in integrations like observability.
After searching the vendor floor at KubeCon last year, CodeFresh was the closest thing I could find to an actual Pipeline tool[^pipeline] - but, as with the other (even more awesome!) technology I discovered at that con ([Crossplane](https://www.crossplane.io/)), I haven't gotten a chance to actually use it at work yet.
# Why Gitea Actions?
A coworker asked this question (in comparison to "_why not self-hosted runners triggering from a GitHub repo?_"), and honestly it was a good question that helped me understand my own motivations better. The justifications I came up with were:
* There are many reasons to self-host, but primary for me is "_reducing dependence on, and power of, centralized providers_". I'm a big believer in the societal merits of many small actors collaborating via network effects, rather than many consumers using a central platform or service; although a single well-funded and -staffed provider can probably provide more features/content than the network example, it invariably comes at the cost of excessive monetization and enrichment of capital, walled gardens, privacy erosion, algorithmic behaviour influencing, and other undesirable outcomes. I don't hold any illusions that my tiny self-hosted forge is even the barest speck of a flicker on GitHub's radar - but leading by example, and living life authentically according to your values, are worthwhile in-and-of themselves even if they have no tangible external effects. And hey, who knows - if my example is enough to help just one other person start investigating ways to break out of the centralized tech monopoly that is [ruining our lives and civilizations](https://www.wheresyoured.at/never-forgive-them/), I'll consider that well worth it.
* A secondary reason I self-host is to learn how things work. This stands in contrast to _making_ things work - that's good as well, of course, but (so long as we're talking about non-essential services) I'd much rather have a semi-functional homelab with a hodge-podge of applications providing one-tenth of the functionality of a professional build, than to run a single command and have it all Just Work™ _without understanding **how** it works_. This is a personal choice, of course - in business, the opposite choice is very often the correct one[^build-your-core-competency].
* This realization also helped me understand my distaste for AI Dev Tools. _Prima facie_, a tool which simply solves the problem for you, without providing understanding to you, is distasteful; a tool that can give you an _incorrect_ understanding is abhorrent. As soon as I consciously recognized this reaction, however, I was quickly able to talk back to it[^mindfulness] - if one approaches AI-generated outputs with an appropriate amount of trust (which should _never_ be 100%), and an awareness for when one is within/outside the bounds of one's own competency, it can be an accelerant to learning as well as to simple execution.
You can of course "_learn things_" at different levels - I could just as well have viewed the "_self-hosted runners_" approach as "_I'm learning how to self-host runners that are triggered from GitHub_". But the former reason was convincing enough.
[^ill-featured]: It's telling that even the author of a [plugin that provides multiarch Docker builds on Drone](https://github.com/thegeeklab/drone-docker-buildx) has moved to another provider.
[^patch-version]: I'm guessing. I jumped straight from `1.20.0` to `1.23.4`, but from the source code it _looks like_ the feature was added in `1.23.0`.
[^infrastructure]: Such as it is - just a Deployment and a Service.
[^pipeline]: Honourable mentions to PipeCD, dishonourable mentions to Argo Kargo.
[^build-your-core-competency]: I can't find it right now, but I remember reading a great blog post asserting that the only thing you should really be building from scratch in a business is The Thing that your company is actually about - the distinguishing feature, the special sauce, the what-have-you that makes you different and (so you hope) better than your competitors. Everything else, buy it off-the-shelf as much as possible, so you can focus as much of your time and energy on the highly-leveraged thing that makes you special[^domain-driven-design]. In the absence of that blog post, I'll share the excellent, and much-aligned, [Choose Boring Technology](https://boringtechnology.club/) talk.
[^mindfulness]: This is your irregularly-scheduled reminder that the techniques of mindfulness have been around for millenia, and independently discovered and propagated at wildly disparate times and places with very little personal gain at stake. If you consider yourself a hardcore rationalist or scientific mind, the correct response to that is not "_that's all woo-woo bullshit, my mind is too ElEvAtEd for it_", it's "_hmm, there is overwhelming evidence to suggest that this is legitimate - I should look into this more_". Personally I like [Ten Percent Happier](https://www.meditatehappier.com/podcast), but work gives [Headspace](https://www.headspace.com/) for free so that's what I'm on right now.
[^domain-driven-design]: Now that I put it like that, it also sounds like the part I disliked most from the "Domain-Driven Design" book - the waffling about Core/Supporting/Generic subdomains - before it moved onto the actually useful and interesting concepts of Language Boundaries and making code objects reflect and represent mental models.

View File

@ -3,7 +3,6 @@ title: "Grafana Oncall"
date: 2022-09-13T10:52:53-07:00
tags:
- homelab
- k8s
- observability
---

View File

@ -1,57 +0,0 @@
---
title: "How to Reach Agreement"
date: 2024-06-21T07:01:25-07:00
draft: true
tags:
- communication
- productivity
---
I'm a big fan of eponymous laws, to the extent of keeping a list of some of my favourites[^favourite-laws] on my personal page on the Wiki of any company I join[^own-your-own-information]. I tend to find that putting a name to a concept makes it easier to conceptualize, remember, and reason about; and anything which someone thinks important enough to put their name to is probably worth consideration[^eponymous-dish].
<!--more-->
For the past few years, I've been mulling over a [perspective](https://fosstodon.org/@scubbo/112598934484845346) that I suspect has some real legs as a helpful tool, and so I humbly present for your consideration the first draft of **Jackson's Law Of Policy[^what-is-a-policy] Alignment**:
> When a group of individuals, acting in good faith to decide on a policy to achieve a shared goal, are in agreement on:
> 1. The goal itself, and the criteria for and definition of success
> 2. The properties of the proposed approaches, including degrees of certainty where doubt exists
> then the group cannot fail to reach agreement.
As it stands, the observation itself is not particularly interesting - "_when people agree, then they agree_" smells like a tautology. Where it gets interesting in in observing the converse - if disagreement on conclusion exists, then this observation enumerates the possible causes.
# Possible causes of disagreement
T_subbed_K - good faith (self-interest), context (some may believe that more or less risk is available)
## Lack of Good Faith
This is both the most common cause, and the hardest to recognize and remedy. All too often, policy-makers are _not_ acting entirely in good faith to whole-heartedly and solely achieve the goal. Manager may choose a solution that allows their team to garner more glory or avoid toilsome work. Legislators may support laws that harm their constituents but benefit themselves. Self-interest abounds, and is powerful.
## Disclarity in goal
It sounds ridiculous to say "_If you don't agree on what the goal is, you won't agree on the solution_", but this happens more often than you might think.
T_subbed_K Internal or External
### Disagreement on context
Part of agreeing on a goal is agreeing on the context in which it exists - the environmental factors like timescale, integrations, dependencies, and so on. One decision-maker might believe that they're trying to find a long-term, sustainable, extensible, comprehensive solution to the problem, and another might believe they're looking for a quick-fix patch which will stem the bleeding for a short time until the problem is ameliorated by other means. In this situation, they will naturally disagree about the properties to prioritize.
# How to address them
T_subbed_K - clarify definitions, reduce to illustrative examples
[^favourite-laws]: They are, in no particular order: [Ashby's](https://en.wikipedia.org/wiki/Variety_(cybernetics)#Law_of_requisite_variety), [Sturgeon's](https://en.wikipedia.org/wiki/Sturgeon%27s_law), [Brandolini's](https://en.wikipedia.org/wiki/Brandolini%27s_law), [Hyrum's](https://en.wikipedia.org/wiki/API#Hyrums), [Goodhart's](https://en.wikipedia.org/wiki/Goodhart%27s_law), [Hoftstadter's](https://en.wikipedia.org/wiki/Hofstadter%27s_law), and [Conway's](https://en.wikipedia.org/wiki/Conway%27s_law), along with the differently-named [Chesterton's](https://en.wikipedia.org/wiki/G._K._Chesterton#Chesterton's_fence). And, while writing this article, I came across [this excellent page](https://hacker-laws.com/) which collects many of the best.
[^own-your-own-information]: which - I'm increasingly realizing as I get older and more ~~cynical~~ realistic - was a mistake. Own the information that you create, in such a way that it is portable when you are no longer convenient to the organization providing you with hosting - and recognize that "_an index of useful information_" is itself useful information!
[^eponymous-dish]: for similar reasons, if I'm eating at a new restaurant and they have a dish named after the establishment, that's usually my default first choice - though I might there be being a sucker for a subtle priming technique...
[^what-is-a-policy]: In my original formulation of this observation, it applied to technical decision-making and system designs. However, I believe it applies to any decision of the form "_What should we do in order to achieve some goal?_"
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,159 +0,0 @@
---
title: "Jellyfin Over Tailscale"
date: 2025-02-21T21:18:31-08:00
tags:
- Cloudflare-tunnels
- Homelab
- Jellyfin
- K8s
- Tailscale
---
I know just enough about computer security to know that I don't know enough about computer security, so I default to keeping my systems as closed-off from the outside world as possible. I use [Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) for the few systems that I want to make externally available[^tunnel-dns] (like [Gitea](https://gitea.scubbo.org)), and [Tailscale](https://tailscale.com/) to access "internal" services or ssh while on-the-go.
<!--more-->
Recently I hit on an interesting problem - giving external access to my Jellyfin server. Cloudflare is pretty adamant that they don't support streaming over Tunnels, so that option was out. Thankfully, Tailscale provides a pretty neat solution. By creating an externally-available "jump host", connecting it to your Tailnet, and using [Nginx Proxy Manager](https://nginxproxymanager.com/) to forward requests from the public Internet to the Tailnet, you can provide externally-available access to an internal service without opening a port.
# Step-by-step instructions
## Prerequisites
* A Tailnet
* A DNS domain that you control - in my case, `scubbo.org`
* An AWS Account, with a VPC and Subnet
* I'm sure similar approaches would work with other Cloud Providers, this is just the one that I'm most familiar with
* Jellyfin running on Kubernetes, with hosts connected to the Tailnet
* Again, I'm pretty sure this would work on some other hosting system, just so long as you have Nginx or something similar to redirect traffic based on their `Host` header.
## Step 1 - Create the proxy host
Deploy the following Cloudformation Template, setting appropriate values for the VpcId and SubnetId:
```yaml
# https://blog.scubbo.org/posts/jellyfin-over-tailscale
AWSTemplateFormatVersion: 2010-09-09
Parameters:
VpcIdParameter:
Type: String
SubnetIdParameter:
Type: String
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: TailnetProxySecurityGroup
GroupDescription: Tailnet Proxy Security Group
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: 443
ToPort: 443
IpProtocol: -1
- CidrIp: 0.0.0.0/0
FromPort: 80
ToPort: 80
IpProtocol: -1
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: 22
ToPort: 22
IpProtocol: -1
- CidrIp: 0.0.0.0/0
FromPort: 80
ToPort: 80
IpProtocol: -1
VpcId:
Ref: VpcIdParameter
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: TailnetLaunchTemplate
LaunchTemplateData:
UserData:
Fn::Base64: |
#!/bin/bash
# https://docs.docker.com/engine/install/ubuntu/
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
cat <<EOF | sudo docker compose -f - up -d
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
restart: unless-stopped
ports:
- "80:80"
- "81:81"
- "443:443"
volumes:
- data:/data
- letsencrypt:/etc/letsencrypt
volumes:
data:
letsencrypt:
EOF
curl -fsSL https://tailscale.com/install.sh | sh
JellyfinProxyInstance:
Type: AWS::EC2::Instance
DependsOn: "LaunchTemplate"
Properties:
ImageId: ami-04b4f1a9cf54c11d0
InstanceType: t2.micro
LaunchTemplate:
LaunchTemplateName: TailnetLaunchTemplate
Version: "1"
NetworkInterfaces:
- AssociatePublicIpAddress: "true"
DeviceIndex: "0"
GroupSet:
- Ref: "SecurityGroup"
SubnetId:
Ref: SubnetIdParameter
```
## Step 2 - Connect the proxy host to the Tailnet
SSH to the host (e.g. via AWS Instance Connect), and run `sudo tailscale up`. Follow the instructions at the resulting URL to connect the machine.
## Step 3 - Configure Nginx Proxy Manager
From a machine already on your Tailnet, connect to `<Tailnet address of the EC2 instance>:81`. Log in with the default credentials of `admin@example.com // changeme` (and follow the instructions to change them immediately!), then:
* Go "Hosts" -> "Proxy Hosts" -> "Add Proxy Host".
* Enter your desired publically-available domain under "Domain Names". Leave "Scheme" and "Forward Port" at the defaults "http" and "80". In "Forward Hostname / IP", enter the Tailscale-name of the host running Jellyfin.
* Check "Block Common Exploits" - [might as well](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/develop/docker/rootfs/etc/nginx/conf.d/include/block-exploits.conf), since the whole point of this is to reduce attack surface. I do have "Websockets Support" enabled, I haven't tested it without.
Note that port 81 is intentionally not exposed via the Security Group - configuring NPM should only be possible from trusted hosts.
## Step 4 - Configure Jellyfin host to accept requests from the publically-available domain
If using a k8s Ingress, add a new entry to the `hosts` array, with `host: <public domain>`, like [this](https://gitea.scubbo.org/scubbo/helm-charts/commit/5e08c653a35314cdf2bf5a1ff3a64d5e44660f2b).
If you're using nginx, I'm sure you can figure that out!
# Possible improvements
* Provide a Cloudformation parameter for a Tailscale Auth Key, allowing the instance to automatically self-authenticate without manually ssh-ing in.
* Preconfigure NPM with the Proxy host rather than needing to configure via the UI.
And, wouldn't you know it - right as I finished writing this blog post, I found out about [Tailscale Funnel](https://tailscale.com/blog/introducing-tailscale-funnel), which seems to do much the same thing. Oh well - this was still a learning experience!
# Why is this preferable to just opening a port?
Honestly...I don't really know. Intuitively it _feels_ safer to have traffic go via an intermediary proxy host and to add a layer of Nginx "_block\[ing\] common exploits_" than to just open up port 80 on my home firewall, but honestly I couldn't tell you why - that is, what attacks this blocks that would otherwise succeed. Like I said, I know enough security to know that there's a ton I don't know. If you have experience or insight here, please let me know!
[^tunnel-dns]: [Here]({{< ref "/posts/cloudflare-tunnel-dns" >}}) is an earlier post on that!

View File

@ -1,51 +0,0 @@
---
title: "Jsonnet in Argocd"
date: 2024-03-12T18:55:26-07:00
draft: true
tags:
- CI/CD
- communication
- end-of-year-wrapups
- homelab
- information-management
- k8s
- leisure
- mathematics
- MentalHealth
- meta
- observability
- politics
- productivity
- programming-challenges
- programming-language-design
- reading
- real-life
- rust
- SDLC
- short-thoughts
- snippets
- transhumanism
- web3
- wordle
---
This is the introduction
<!--more-->
And this is the rest of the content
Notes for post:
* [Main page](https://jsonnet.org)
* [Helpful reference](https://dev.to/kubeden/jsonnet-adventures-deploying-our-application-to-argocd-4fk2)
* [ArgoCD diff](https://codefresh.io/blog/argo-cd-preview-diff/)
* Gotcha that the `argocd` CLI tool doesn't work unless you are in the right Kubernetes namespace - otherwise you get `FATA[0000] error retrieving argocd-cm: configmap "argocd-cm" not found`
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,19 +0,0 @@
---
title: "Keycloak Backup"
date: 2024-04-06T17:34:34-07:00
tags:
- homelab
- keycloak
- k8s
---
Setting up regular backup for my [Keycloak installation]({{< ref "/posts/oidc-on-k8s" >}}) was a lot trickier than I expected!
<!--more-->
Although there is a `kc.sh export` command on the image, there's a [long-standing bug](https://github.com/keycloak/keycloak/issues/14733) whereby the export process and the server clash for the same port. I went [down the rabbit-hole](https://github.com/keycloak/keycloak/issues/28384) with the Keycloak folks trying to workaround that - only to realize that, because the image doesn't come with `cron` installed, I wouldn't be able to schedule the `kc.sh export` on the main pod _anyway_, but would have to schedule it externally.
A [Kubernetes CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/) was the obvious solution - but there were several hoops to jump through first:
* I needed to replicate an `initContainer` which, so far as I can tell, just [copies a directory](https://github.com/bitnami/charts/blob/main/bitnami/keycloak/templates/statefulset.yaml#L100) into [a PV](https://github.com/bitnami/charts/blob/main/bitnami/keycloak/templates/statefulset.yaml#L113-L115), only for that same PV [to get mounted at the original path again in the main container](https://github.com/bitnami/charts/blob/main/bitnami/keycloak/templates/statefulset.yaml#L284-L286)
* I couldn't just run `kc.sh export` as I had on the primary pod, but had to explicitly pass `--db`, `--db-username`, and `--db-password`. This is _probably_ self-evident if you understand the architecture of Keycloak, but wasn't obvious to me - the initial attempts to export from the main pod were failing because of a port clash, so "obviously" (scare-quotes because this is apparently wrong!) it was pulling data from some external datasource rather than from a local source.
* Since I was having the CronJob write out to an NFS-mounted volume (on my NAS), I needed to specify `securityContext.runAsUser` and `securityContext.fsGroup` on the `container`, and ensure that the corresponding values were set on the directory in the NAS's local filesystem, otherwise the CronJob would be denied write permission (thanks to [this SO question](https://stackoverflow.com/questions/50156124/kubernetes-nfs-persistent-volumes-permission-denied) for helping me figure this out - NFS permissions are beginning to make sense to me, but I'm still getting my head around it!)
My solution is [here](https://gitea.scubbo.org/scubbo/helm-charts/src/branch/main/app-of-apps/keycloak-backup.yaml). It's not perfect (I'd love to find a way to run `$(date +%s)` in the `args` to name the files according to date, and this setup breaks the neat "app-of-apps" setup I have going because this didn't seem deserving of a full Chart setup), but it works! It'd be really cool to contribute this to the [Bitnami Chart](https://github.com/bitnami/charts/tree/main/bitnami/keycloak) - I'm imagining a `backup` namespace in the `values.yaml` specifying schedule, persistent volume specs, and realms. Shouldn't be _too_ hard...

View File

@ -1,88 +0,0 @@
---
title: "OIDC on K8s"
date: 2024-04-01T20:36:32-07:00
tags:
- dns
- homelab
- keycloak
- k8s
---
I just configured OIDC login for the first service on my Homelab.
<!--more-->
The first step was picking a provider - but thanks to the [awesome self-hosting guide](https://github.com/awesome-foss/awesome-sysadmin?tab=readme-ov-file#identity-management---single-sign-on-sso), I'd already narrowed it down to a shortlist, and [this post](https://old.reddit.com/r/selfhosted/comments/ub7dvb/authentik_or_keycloak/) helped me pick Keycloak over Authentik.
Unusually, there's no Helm chart listed in the [getting started guide](https://www.keycloak.org/getting-started/getting-started-kube), but the old standby of [Bitnami](https://github.com/bitnami/charts/tree/main/bitnami/keycloak) had an offering (though they did weirdly change the admin username from `admin` to `user`, which threw me off at first). [Installation via GitOps](https://gitea.scubbo.org/scubbo/helm-charts/commit/1d56a131b71315fb3c1fb2a3b2b39d099b0f605d) was a breeze now that I'm using [jsonnet](https://jsonnet.org/) to extract common Application setup boilerplate - though I did have to upgrade my ArgoCD installation from `2.7` to `2.10` to make use of `valuesObject` configuration.
The first application I wanted to integrate was Argo itself[^jellyfin-plugin], and thankfully there's a step-by-step [guide](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/keycloak/) available, which..._mostly_ worked[^realm].
## [It's not DNS...](https://www.cyberciti.biz/media/new/cms/2017/04/dns.jpg)
I did run into a problem, though - I'd entered an override on my OpnSense router (running Upbound DNS) for `keycloak.avril`[^avril] pointing to the k8s cluster, so that I could access it from my browser - but, apparently, the pods on the cluster don't delegate to that resolver, so I got an error `Failed to query provider "http://keycloak.avril/realms/avril": Get "http://keycloak.avril/realms/avril/.well-known/openid-configuration": dial tcp: lookup keycloak.avril on 10.43.0.10:53: no such host` when trying to login via SSO. At first I tried setting the `issuer` value in Argo's `oidc.config` to `http://keycloak.keycloak` rather than `http://keycloak.avril` (i.e. using the k8s internal DNS name for the service), which allowed Argo to talk to Keycloak, but then gave a DNS error when my _browser_ tried to connect to that host. I could have worked around this by also setting a `keycloak.keycloak` DNS override on the OpnSense Unbound resolver, but that felt hacky - and, besides, I wanted to understand Kubernetes' DNS setup a little better.
[This SO answer](https://stackoverflow.com/a/65338650/1040915) looked promising as a way to set overrides for k8s' CoreDNS - but, since my ConfigMap already had a `hosts` entry (presumably provided by [k3s](https://k3s.io/)?):
```
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts /etc/coredns/NodeHosts {
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
}
import /etc/coredns/custom/*.server
NodeHosts: |
192.168.1.70 host1
192.168.1.71 host2
...
```
I got an error `plugin/hosts: this plugin can only be used once per Server Block` when trying to add another (I'm not sure why that restriction exists tbh - the [docs](https://coredns.io/plugins/hosts/) make it clear that the plugin can be limited to a zone, so it seems reasonable to have multiple entries for multiple zones?). Handily, though, the plugin also allows listing overrides inline, so I was able to add an entry for `keycloak.avril` and everything worked as-desired!
```
...
hosts /etc/coredns/NodeHosts {
192.168.1.70 keycloak.avril
ttl 60
reload 15s
fallthrough
}
...
...
```
That worked, but still felt hacky. Now I was managing DNS overrides in two places rather than one. The docs do list a `forward` [plugin](https://coredns.io/manual/configuration/#forwarding) which looks like it should do what I want - but adding that (and removing the manual override in `hosts`):
```
...
forward avril 192.168.1.1 # My OpnSense router IP
forward . /etc/resolv.conf
...
```
...gave a slightly different error `failed to get token: Post "http://keycloak.avril/realms/avril/protocol/openid-connect/token": dial tcp: lookup keycloak.avril on 10.43.0.10:53: no such host` during callback in the OIDC process. Even opening up the `forward` to operate for all names (`.`) failed (though in this case it was back to the `Failed to query provider...` error).
🤷🏻‍♂️ at some point you've just gotta take a working solution (the inlined entry in `hosts`) and move forwards with it! This duplication isn't _too_ bad - I doubt there'll be _another_ system (other than OIDC) where I'll need both pods and my browser to be able to use the same DNS name. If there is, I'll return to this problem and try to crack it.
I do also see an `import /etc/coredns/custom/*.override` line in that configuration, which would be another promising avenue of investigation - and, hey, if I realize my intention of managing Unbound DNS entries via [Crossplane](https://www.crossplane.io/), both the Router Overrides and the CoreDNS configuration could be generated from the same source.
[^jellyfin-plugin]: Jellyfin would probably be next, though it looks like that's not natively supported and requires a [plugin](https://github.com/9p4/jellyfin-plugin-sso), or maybe "Keycloak + OpenLDAP" as per [here](https://old.reddit.com/r/selfhosted/comments/ed1z9e/sso_with_authorization_for_jellyfin_ombi_sonarr/fbffkfp/) - though at this point I haven't researched the difference between LDAP and SSO.
[^realm]: _Don't_ follow their instructions to work in the default realm `master`, though! [Keycloak docs](https://www.keycloak.org/docs/latest/server_admin/#the-master-realm) make it clear that you should "_Use the `master` realm only to create and manage the realms in your system._"
[^avril]: As long-time readers will remember, the name that my partner and I use for our house - and so, the name I use for any domain/realm/namespace/zone on our network - is "Avril", because the house purchase process _went and made things so complicated..._

View File

@ -1,53 +0,0 @@
---
title: "Pre-Pipeline Verification, and the Push-And-Pray Problem"
date: 2023-11-23T16:26:06-08:00
tags:
- CI/CD
- SDLC
---
It's fairly uncontroversial that, for a good service-deployment pipeline, there should be:
* at least one pre-production stage
* automated tests running on that stage
* a promotion blocker if those tests fail
The purpose of this testing is clear: it asserts ("_verifies_") certain correctness properties of the service version being deployed, such that any version which lacks those properties - which "is incorrect" - should not be deployed to customers. This allows promotion to be automated, reducing human toil and allowing developers to focus their efforts on development of new features rather than on confirmation of the correctness of new deployments.
<!--more-->
There's plenty of interesting nuance in the design of in-pipeline testing stages, but in this post I want to talk about the testing you do _before_ a pipeline - and, particularly, why it's important to be able to run Deployed Tests before submitting code.
## Definition of Deployed Testing
Categories of test are a fuzzy taxonomy - different developers will inevitably have different ideas of what differentiates a Component Test from an Integration Test, or an Acceptance Test from a Smoke Test, for instance - so, in the interests of clarity, I'm here using (coining?) the term "Deployed Test" to denote a test which can _only_ be meaningfully carried out when the service is deployed to hardware and environment that resembles those on/in which it runs in production. These typically fall into two categories:
* Tests whose logic exercises the interaction of the service with other services - testing AuthN/AuthZ, network connectivity, API contracts, and so on.
* Test that rely on aspects of the deployed environment - service startup configuration, Dependency Injection, the provision of environment variables, nuances of the excecution environment (e.g. Lambda's Cold Start behaviour), and so on.
Note that these tests don't have to _solely, specifically, or intentionally_ test characteristics of a prod-like environment to be Deployed Tests! Any test which _relies_ on them is a Deployed Test, even if that reliance is indirect. For instance, all Customer Journey Tests - which interact with a service "as if" a customer would, and which make a sequence of "real" calls to confirm that the end result is as-expected - are Deployed Tests (assuming they interact with an external database), even though the test author is thinking on a higher logical level than confirming database connectivity. The category of Deployed Tests is probably best understood by its negation - any test which uses mocked downstreams, and/or which can be simply executed from an IDE on a developer's workstation without any deployment framework, is most likely not a Deployed Test.
Note also that, by virtue of requiring a "full" deployment, Deployed Tests typically involve invoking the service via its externally-available API, rather than by directly invoking functions or methods as in Unit Tests.
## When do we do Deployed Testing? When _should_ we do it?
Deployed Testing most naturally occurs in the CD pipeline for the service. If you were to list the desired properties of a pipeline, right at the top would be "_It builds the application and deploys it to production_", but right below that would be "_...but before doing so, it deploys to a testing stage and runs tests to make sure the deployment to production will be safe_".
However! All too often I see this being the _only_ way that teams are able to run Deployed Tests - that they are literally unable to:
* create a deployment of the application whose artifact was built from the state of code currently on their local development machine
* run a Deployed Test suite against that deployment, where the logic of the tests again is determined by the state of code on their machine
The thinking seems to be that Deployed Tests will be executed in the pipeline _anyway_, so there's no point in running them beforehand - any "bad changes" will get caught and rolled back, so production will be protected. And this is true! But, by leaving the detection of issues until the last minute - when the change is _in_ the (single-threaded) pipeline and when any test failures will block other changes coming down the pipe; when other developers may have started developing against the changes already merged - the disruption of a failure is significantly higher. For low-confidence changes which relate to properties that are only testable in a Deployed Environment, developers have to "Push And Pray" - "_I **think** that this change is correct, but I have no way of verifying it, so I need to push it into the pipeline before I can get any feedback_". This cycle - push, observe failed test results, make local change, push again - might repeat multiple times before they get to working code, during which time the whole pipeline is unusable. They are effectively making the whole pipeline their personal development environment, blocking anyone else from deploying any changes or even making any code changes which depend on their (unstable) merged code.
It's a small amount of extra effort, but it's _entirely_ worthwhile to set up the ability described in the preceding bullet points, whereby developers can run locally-defined tests against a locally-defined service[^running-locally] before even proposing the change for merging to `main`. Note that this testing is worthwhile in both directions - not only can the dev run existing tests against a new AppCode change to confirm that it's correct, but they can also run a new version of the **Test**Code against existing AppCode to ensure that it operates as-expected!
## Ephemeral Environments are great, but are not enough
A closely-related topic is "_building and deploying the code associated with a Pull Request, running tests against it (and reporting on them in the Pull Request), and providing a URL where stakeholders can experimentally interact with the service (or, more commonly, website)_" (I don't know of a general term for this, but it's called "Ephemeral Environments" at my current workplace, hence the section title). This is a great practice! Anything you can do to give high-quality testing _early_ in the SDLC - critically, _before_ merging into `main` (after which the impact of a rollback or correction is much higher) - is valuable, particularly if it involves getting explicit signoff from a stakeholder that "_yep, that was what I expected from this change_".
However, there should be no need to involve a remote repository system (GitHub etc.) in the process of creating and testing a personal deployment. It _works_, but it's an extra step of unnecessary indirection:
* For any non-Cloud-based system, running an instance of the application from code you have built locally should be trivial - if it's not just `docker build ... && docker run ...`, there should be a very small number of scriptable steps.
* Even for apps that deploy to AWS, GCP, or another Cloud Provider, it should be possible to locally-build AppCode updates, and push the Docker image (or other artifact) to your personal testing deployment without getting GitHub/CodeCommit/CodePipeline involved.
* Testing of infrastructure changes are a little trickier, but depending on your IaC configuration _could_ still be possible - though at that point the creation of a deployment pipeline _for_ a personal testing environment is probably worthwhile.
Don't get me wrong, PR-related Ephemeral Environments are excellent for what they are, and I heartily recommend them - but if you don't know how to build and deploy your application _from your laptop_ without getting GitHub involved, you probably don't know[^knowledge-is-distributed] it well enough to properly operate it at all. Or, you may be [over-applying GitOps](https://fosstodon.org/@scubbo/111112129591386185) under the mistaken assumption that _nothing_ about _any_ system, _anywhere_, should _ever_ be changed triggered by _anything_ except by a change to a Git repo. That's not even true for production systems[^not-everything-is-gitops], so it's _certainly_ not true for development systems which have made the trade-off of flexibility and agility at the cost of stability. By all means insist, on a rigorous, centralized, standardized, high-confidence, reproducible, audit-logged process (i.e. a GitOps-y one) for everything _after_ "merge to `main`" (and _especially_ regarding "deploy to `prod`) - but, for everything before that point in the SDLC, prefer agility and fast-feedback with as few moving parts as possible.
[^running-locally]: ideally, but not necessarily, _running_ locally as well - though if there are aspects of the deployment environment that mean this is impractical (like depending on Cloud resources, large scale, or particular architecture), this isn't necessary
[^knowledge-is-distributed]: where the definition of "know" is a little fuzzier than just "_have the knowledge immediately to-hand in your mind_". If that "knowledge" consists of "_I know the script I need to run_", then that's good enough for me - it can live in your "_exobrain_", the collection of cognition- and memory-enhancing/supporting tools and structures that you use to augment your natural human brain.
[^not-everything-is-gitops]: when a customer changes their settings in the Web UI, is that change stored into a Git Repo before being reflected? No, it just gets written to a database? OK, so you acknowledge that _some_ properties of the system can have authoritative sources that are not Git repos - now we're just quibbling about where the appropriate dividing line is drawn. Personally I have long believed that "_which image/version is deployed to which stage of a pipeline?_" is properly viewed as an emergent runtime property of the-pipeline-viewed-as-a-software-system-itself, rather than a statically-(Git-)defined property of the application - it is State rather than Structure - but to fully explore that deserves its own post.

View File

@ -1,97 +0,0 @@
---
title: "Project Management and Async Functions"
date: 2024-02-20T21:32:49-08:00
tags:
- homelab
- programming-language-design
- SDLC
---
In my greatest display yet of over-engineering and procrastinating-with-tooling, I've started self-hosting [OpenProject](https://www.openproject.org/) to track the tasks I want to carry out on my homelab (and their dependencies).
<!--more-->
![Screenshot of the OpenProject UI](/img/open-project-screenshot.png "Pictured - a very normal and rational and sensible thing to do")
Annoyingly, I didn't find out until _after_ installation that this system [lacks the main feature](https://community.openproject.org/topics/8612) that made me want to use a Project Management Solution™ over a basic old Bunch Of Text Files - dependency visualization and easy identification of unblocked tasks.
Fortunately, the system has an API (of course), and some time later I'd whipped up this little "beauty" to print out all the unblocked tasks (i.e. all those I could start work on immediately):
```python
#!/usr/bin/env python
import json
import os
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROJECT_ID=<id>
BASE_URL='http://url.to.my.openproject.installation'
API_KEY=os.environ['API_KEY']
def main():
all_tasks = _req(f'api/v3/projects/{PROJECT_ID}/work_packages')['_embedded']['elements']
unblocked_tasks = [
{
'id': elem['id'],
'title': elem['subject'],
'href': f'{BASE_URL}/work_packages/{elem["id"]}'
} for elem in all_tasks
if _task_is_unblocked(elem['id'])
]
print(json.dumps(unblocked_tasks, indent=2))
def _task_is_unblocked(task_id: int) -> bool:
relations_of_task = _req(f'api/v3/work_packages/{task_id}/relations')['_embedded']['elements']
urls_to_blockers_of_task = [
relation['_links']['from']['href']
for relation in relations_of_task
if relation['type'] == 'blocks'
and relation['_links']['to']['href'].split('/')[4] == str(task_id)]
return all([
_req(url)['_embedded']['status']['isClosed']
for url in urls_to_blockers_of_task])
def _req(path: str):
return requests.get(f'{BASE_URL}/{path}', auth=('apikey', API_KEY), verify=False).json()
if __name__ == '__main__':
main()
```
(Yes, I haven't installed TLS on my cluster yet. The task's right there in the screenshot, see!?)
This is, of course, inefficient as can possibly be\[citation needed\], as it doesn't use any parallelization for the _many_ network calls, nor any caching of often-referenced data. That's fine for now, as N is going to be real small for quite some time.
## Async functions as first-class language design
That actually gets me onto a different topic. For some years now I've enjoyed, shared, and referenced the [What Color Is Your Function?](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) article, which (spoilers - seriously, if you are a software engineer, go read it, it's good!) points out the ways in which building-in `async` functions to a language make things really awkward when the rubber hits the road. For a long time I really resented this `async` annotation (which I first encountered in JavaScript, but I then found out it has spread to [Python](https://docs.python.org/3/library/asyncio-task.html), too), as to me it seemed like unnecessary extra overhead - why should I have to annotate _every_ function in my call-stack with `async` just because they called an asynchronous function at some point in the stack? Why, in the following snippet, does `top_level()` have to be `async`, when all it's doing is a synchronous operation on an blocking function?
```python
#!/usr/bin/env python
import asyncio
async def top_level():
print(await mid_level() * 2)
async def mid_level():
return await bottom_level() + 1
async def bottom_level():
# Imagine that this called out to the network
# or did some other actually-async operation
return 1
if __name__ == '__main__':
asyncio.run(top_level())
```
I recently read [this article](https://blainehansen.me/post/red-blue-functions-are-actually-good/) which made the interesting case that `async` should be thought of as a member of the Type System, surfacing information about the behaviour of the associated function:
> Colored functions reveal important realities of a program. Colored functions are essentially a type-system manifestation of program effects, all of which can have dramatic consequences on performance (unorganized io calls can be a latency disaster), security (io can touch the filesystem or the network and open security gaps), global state consistency (async functions often mutate global state, and the filesystem isn't the only example), and correctness/reliability (thrown exceptions are a program effect too, and a `Result` function is another kind of color). Colored functions don't "poison" your program, they inform you of the _reality that your program itself_ has been poisoned by these effects.
I...can see where they're coming from, I guess? According to this viewpoint, `mid_level` should still be declared as `async`, even though it `await`s the actually-asynchronous function, because...the "network-call-ingness" of `bottom_level` propagates up to `mid_level`? I hadn't thought of it that way, but I can see that that's true. My local definition of `mid_level` does nothing asynchronous, but asynchonicity is transitive.
Not gonna lie, though, I still find the experience of writing `async`-ified code really frustrating. I begin writing out my logic in terms of (normal) functions and their interactions, traversing down/through the logic tree from high-level concepts down to implementations of API calls - at which point I hit a network call, and then am forced to traverse back up the tree scattering `async`/`await`s everywhere where I previously had normal function declarations and invocations. Maybe - and I'm half-joking, half-serious here - I should just _start_ writing the program "as if" it was going to be asynchronous in the first place? I wonder what would change then.

View File

@ -1,47 +0,0 @@
---
title: "PVC Debug Pod"
date: 2024-03-04T22:05:41-08:00
tags:
- k8s
---
I've been annoyed sufficiently-often by the fact that there is no single `kubectl` command to "_create a pod, and attach a PVC to it_" that I threw together the following script:
<!--more-->
```bash
#!/bin/bash
set -ex
# This script assumes the existence and correct configuration of `kubectl` and `fzf`.
# TODO - cool feature would be to grab namespaces with `kubectl get ns` and pipe through `fzf` to select - but, 99% of the time, this'll just be for the current namespace anyway
PVC_TO_MOUNT=$(kubectl get pvc --no-headers | awk '{print $1}' | fzf)
POD_CREATE_OUTPUT=$(cat <<EOF | kubectl create -f -
apiVersion: v1
kind: Pod
metadata:
generateName: debug-pod-
spec:
volumes:
- name: pvc
persistentVolumeClaim:
claimName: $PVC_TO_MOUNT
containers:
- name: debug-container
image: ubuntu
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
volumeMounts:
- mountPath: "/mnt/pvc"
name: pvc
EOF
)
POD_NAME=$(echo $POD_CREATE_OUTPUT | awk '{print $1}')
kubectl wait --for=condition=Ready $POD_NAME
kubectl exec -it $POD_NAME /bin/bash
```
While researching it, I did find out that [Ephemeral Containers](https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/) are now a thing - but, given that they also don't appear to allow a PVC-mount in their `kubectl`-creation, I suspect you'd still have to create via `cat <<EOF | kubectl create`[^why-create] anyway.
[^why-create]: Why `create` and not `apply`? Because you can't use `generateName` with `apply`, and if I accidentally forget to tear down an pre-existing debug-pod I'd rather not be interrupted in what I'm doing. Arguably, though, that would be a good reminder to clean up after myself.

View File

@ -4,7 +4,6 @@ date: 2023-02-07T19:52:44-08:00
tags:
- CI/CD
- homelab
- k8s
- observability
---

View File

@ -3,7 +3,6 @@ title: "Secure Docker Registry"
date: 2022-07-01T21:26:32-07:00
tags:
- homelab
- k8s
---
Part of the self-hosted setup that supports this blog (along with all my other homelab projects) is a [Docker Registry](https://docs.docker.com/registry/) to hold the images built and used in the CI/CD pipeline. Recently I tried to install TLS certificates to secure interaction with the Registry, and it was a fair bit harder to figure out than I expected, so I wanted to write it up both for future-me and for anyone else struggling with the same problem.
<!--more-->

View File

@ -3,7 +3,6 @@ title: "Self-Hosted Analytics"
date: 2022-08-02T20:23:48-07:00
tags:
- homelab
- k8s
- meta
---

View File

@ -5,7 +5,7 @@ tags:
- meta
- homelab
---
Despite this blog being [initially set up to primarily talk about self-hosting]({{< ref "/posts/my-first-post" >}}), I'd actually been hosting it on AWS until very recently. This was due to caution - I know just enough about security to know that I know next-to-nothing about security, and so I didn't want to expose any ports on my _own_ network to the Internet. Instead, I set up [an AWS CodePipeline](https://github.com/scubbo/blogCDN) to build the blog and deploy to S3 anytime I pushed a new change. Admittedly, this was a pretty cool project in itself that taught me a lot more about CDK and some AWS services; but it didn't feel like true self-hosting, even though I wasn't using anything like Medium or WordPress.
Despite this blog being [initially set up to primarily talk about self-hosting]({{< ref "/posts/my-first-post." >}}), I'd actually been hosting it on AWS until very recently. This was due to caution - I know just enough about security to know that I know next-to-nothing about security, and so I didn't want to expose any ports on my _own_ network to the Internet. Instead, I set up [an AWS CodePipeline](https://github.com/scubbo/blogCDN) to build the blog and deploy to S3 anytime I pushed a new change. Admittedly, this was a pretty cool project in itself that taught me a lot more about CDK and some AWS services; but it didn't feel like true self-hosting, even though I wasn't using anything like Medium or WordPress.
<!--more-->
Thanks to [this great blog post](https://eevans.co/blog/garage/), I found an alternative that allowed me to feel the nerd-pride of self-hosting while retaining security. [Cloudflare](https://www.cloudflare.com/) offer a free service called "_Cloudflare Tunnel_" (formerly "_Cloudflare Argo_"). To quote that blog:

View File

@ -2,8 +2,6 @@
title: "Short Thoughts 2022-03-05"
date: 2023-03-05T19:49:27-08:00
tags:
- programming-challenges
- rust
- short-thoughts
---

View File

@ -1,35 +0,0 @@
---
title: "Short Thoughts 2023 05 29"
date: 2023-05-29T10:37:45-07:00
draft: true
tags:
- short-thoughts
---
What's the saying - "_If you want to make God laugh, tell him your plans_"? My last Short Thoughts post - where I was "_trying to keep up the routing of writing at least one of [these]({{< ref "/posts/consistency-in-2023" >}}) a week_" - was [nearly three months ago]({{< ref "/posts/short-thoughts-2023-03-05" >}}). Ah well, no use in beating myself up over it!
<!--more-->
## The Rust is still there
As in that previous post, I'm still wrestling with Rust. I finished working through [The Rust Book](https://rust-book.cs.brown.edu/) a couple weeks back, which helped a _lot_ with clarification of the important concept I was missing. The _idea_ of borrow-checking now makes perfect sense to me, and the promised outcome (all the speed of non-GC'd languages with none of the unsafety) is tantalizing - but I'd be lying if I claimed not to be frustrated by how bloody difficult it makes trivial operations like concatenating two strings or retrieving a value from a HashMap. Inspired by [Julia Evans' post on the pitfalls of floating-point numbers](https://jvns.ca/blog/2023/01/13/examples-of-floating-point-problems/), I tried throwing together an implementation of arbitrary[^arbitrary] precision numbers [here](https://gitea.scubbo.org/scubbo/rust-real-numbers). Still very much a work-in-progress - so far I've only implemented Addition, Subtraction, and Equality, there's no explicit testing of the "faults" of floating-point numbers nor of the performance, and I've already noticed a couple of points where I could improve the design. That said - if you are a talented Rustacean, all advice is welcome!
Tangentially, I note that the [Rust community](https://www.jntrnr.com/why-i-left-rust/) is going through some controversy. This is disappointing, as until now it had always seemed like an intentionally-crafted, inclusive and positive community. Here's hoping this is (as one HN commenter said) "_a moment the Rust community can look back on as a time that they really lived their values \[by doing the right thing\]._".
## In other news
Work at my new place (LegalZoom) is still going well. The role is "SRE", but the tech-support situation is such that we're trying our hand at Security, DevOps, and Builder Tools as well. I'm getting to wear many hats, and really enjoying it! Finally getting around to reading [Domain-Driven Design](https://amzn.to/3qkeHUk) after a coworker's evangelism. 1/4 of the way in, honestly most of it seems like self-evident truisms, which I guess is indirectly an extremely high compliment to a book that came out 20 years ago - it's been so enthusiastically adopted that what was once revolutionary is now accepted wisdom. I'm hoping that deeper insights will arise as I keep on.
Speaking of my new role - man, I still really miss the Builder Tools at Amazon. [Pipelines](https://blog.scubbo.org/posts/ci-cd-cd-oh-my/) remains one of the smoothest and most productivity-multiplying tools I've ever used[^disagree]. A unified build system and established deployment mechanism were a godsend for developers working on new services - even more so for an SRE trying to build tools atop them! Don't get me wrong, it's exciting to be able to build them all from scratch - but every so often I do want something to Just Work 😊
I am warming up to the concept of release branches, though. At first, they seemed like nonsense - why would you cut a branch in order to release, why aren't you releasing "_the latest build that passes tests_" immediately, that's exactly what CD should be? Having seen the mess that can result from shared testing environments, no pre-pipeline testing, and general lack of accountability for deploying irresponsibly, I can see the value in _temporarily_ having a little more process around releasing _until_ we address all those things.
I still think, though, that any deployment mechanism that relies on a human _trying_ to replicate the code-state from environment N into environment N+1, rather than a machine reliably doing so automatically, is crazytown banana-pants, though.
Look at that, it's been nearly 45 minutes for this "short thought" which is meant to be only 15 minutes. Back to the Rust mines I go!
## But one last thing
I updated the formatting on the [tags page](https://blog.scubbo.org/tags/) so there are three boxes alongside each other, rather than one taking up all the horizontal space. Thanks to my buddy Jeff for the help there! (Link to his site to come once he's set up his home-server 👀)
[^arbitrary]: well - technically it only supports numbers up to `INT_MAX` and down to `2147483647` decimal places. I could probably go to smaller(/arbitrary) precision if I used a List of Maps for the decimal places rather than a single one...
[^disagree]: sorry Ziggy, I still disagree with literally every thing you said [here](https://twitter.com/ZiggyTheHamster/status/1577076232243380230) - Pipelines _does_ do the things you're saying it doesn't do, it just does reflects them in non-standard ways, but the results are the same. It's a double standard to claim that Pipelines "doesn't" do those things because it calls out to other systems (CodeBuild, CodeDeploy, etc.), while simultaneously claiming that (say) Jenkins, GitHub Actions, or Circle "do" them when in fact they are just orchestrators for other chunks of logic too.

View File

@ -1,43 +0,0 @@
---
title: "Snowboarding Philosophy, and A Linguistic Tangent"
date: 2025-04-09T17:54:09-07:00
tags:
- Real-Life
- Snippets
---
Depending on how you look at it, I just finished up either the worst or the best snowboarding season of my life so far.
<!--more-->
It was The Worst Season because:
* Snowfall was the sparsest I'd ever experienced, and the timing of my trips always seemed to coincide with bad weather or sparse snow. Generously, only 2 of the days could be described as having good snow.
* There were more than the usual number of mishaps, misadventures, and gear failures - I had to replace my ski pants, gloves, and helmet due to a combination of failure or misplacement.
* I had the worst fall I've ever taken on a snowboard - so bad I was worried about a concussion afterwards (I was, thankfully, fine) and sported a black eye for a few days afterwards.
But it was The Best Season because:
* I set a new lifetime record for most vertical distance boarded in a season and for top speed in a season[^ski-tracks].
* I discovered the simple joy of listening to a playlist with in-helmet speakers.
* I experienced the most improvement of technique I've ever noticed - both conscious execution of deliberate technique, and unconcious moments of my body intuitively doing "_the right thing_" before my mind could even recognize there was anything to respond to. Both are great feelings; and the latter is an important reminder of the existence and malleability of the subconcious mind.
There's a [Discordian](https://www.learnreligions.com/discordianism-95677) example there on how any and all interpretations are true and no truth is inherent, but I wanted instead to record an observation I made[^truthfulness] during this season about The Philosophy Of Snowboarding[^motorcycle]:
> Something I really like about snowboarding is that it physically forces you to embrace (or at least acknowledge) a philosophy of not letting fear dictate your actions, and of using gentle fluid redirection rather than brute force to achieve your aims.
>
> When going fast, the two most dangerous things you can do are:
> * to give in to the instinct to lean back away from the speed - the board will slip out from under you and you'll lose control and fall
> * to try to force yourself to slow down by aggressively planting an edge across the direction of your travel - any kind of bump will make you judder, lose control, catch a *front* edge, and faceplant
>
> Instead, you need to lean *into* the direction of travel, but smoothly redirect it sideways so the speed gets dampened by moving in a different direction, away from the fall line.
# A linguistic tangent
Improvements to my snowboarding technique reminded me of a concept for which I've long sought a name. A definition of the concept would be "_a simple piece of advice that can be usefully applied in different ways, across a wide range of experience-levels of a discipline_". Typically, though perhaps not always, I expect these would arise in physical disciplines - examples I've often used for snowboarding, tennis, and martial arts are "_weight on the front foot_", "_keep your eye on the ball_", and "_bend your legs_", respectively - although I expect there are probably examples in Software Engineering and other mental disciplines too.
Crucially, this isn't something which is always trivially and obviously true - "_go faster than your opponent and you will win the race_" doesn't strike me as something that can be **re**-interpreted or rediscovered as you become a more proficient racer[^antidote]. I'm talking here about a deceptively-simple concept which a beginner might think they have understood and internalized fully, and have progressed beyond the need to remember; only to encounter a challenging sub-skill or technique which requires them to re-examine the simple advice and apply it in a new context.
I remember this concept every year or so and poll my linguistically-aligned friends for a name, but I don't think I've ever heard a suitable suggestion. At least now I'll have a blog post to point them to next year rather than having to explain it from scratch!
[^ski-tracks]: shout out [SkiTracks](https://www.skitracks.com/) for being a rock-solid application and never succumbing to microtransactions, monetization, or any other bullshit.
[^truthfulness]: that pesky "_commitment to truth_" mindset that I posess compels me to acknowledge that this is not in fact a direct quotation, but has been paraphrased to elaborate.
[^motorcycle]: which reminds me, I'm well overdue to re-read [Zen and the Art of Motorcycle Maintenance](https://en.wikipedia.org/wiki/Zen_and_the_Art_of_Motorcycle_Maintenance) - I suspect that "mid-30's me", having worked in Software Engineering for most of my adult life, will have an even deeper appreciation for it than "late-teens me" did.
[^antidote]: though it _could_ potentially be an antidote to overly-complex techniques or mindsets which stray from the core aim - instead of a koan which generates insight by having no answer, a, uhh, "naok"(?) could generate insight by being _so_ obviously true as to prompt you into re-examining assumptions which appear to contradict it.

View File

@ -28,7 +28,7 @@ Note that I had to put ` - {{ .Page.Title }}` on the _same_ line as the `{{rang
Honestly, I'm a little surprised that this works in the first place. I would have expected that fetching "_a list of all extant tags_" would require building the site, but I've confirmed that this template still works when no `public/` directory (the usual location of built output of a Hugo site) exists. Either the list of extant tags is stored as metadata somewhere (though a `grep -r` didn't find it), or Hugo quickly parses through all existing posts to gather a list of tags when creating a new post from an archetype[^3].
On the topic of tags, I have a couple of improvements I want to introduce:
* The [tags](https://blog.scubbo.org/tags/) page is pretty poorly-laid out, taking up a lot of space for the tag name and showing the preview of the blog posts under it. This means that, on my standard Macbook screen, you can just about see 3 posts'-worth of content (that is - a single tag) - not ideal! ~~It'd be great to restructure this so that the tag name is much smaller, and only the titles of posts under it are shown (maybe with an option to hover to see preview), allowing more content to be readable from a single screenful.~~ Done, as of 2023-05-17, thanks to my buddy Jeff the front-end wizard!
* The [tags](https://blog.scubbo.org/tags/) page is pretty poorly-laid out, taking up a lot of space for the tag name and showing the preview of the blog posts under it. This means that, on my standard Macbook screen, you can just about see 3 posts'-worth of content (that is - a single tag) - not ideal! It'd be great to restructure this so that the tag name is much smaller, and only the titles of posts under it are shown (maybe with an option to hover to see preview), allowing more content to be readable from a single screenful.
* It looks like you can [attach metadata](https://gohugo.io/content-management/taxonomies/) to tags, too - that might be good to do (and to show as a subtitle for the tag) to clarify what a particular tag encompasses (no, `#meta` is _not_ related to The Company Formally Known As Facebook!)
* I'd like to try messing with the ordering[^4] on that tags page, too. At a glance, it looks like it's sorting by "_most recent post within that tag_", which...I guess makes sense. My first thought was that ordering by "_number of posts in tag_" might make more sense (putting "larger" tags above smaller ones), but that might lead to discoverability problems if I write a bunch of posts under one tag and then it's harder to find later ones. Then again, later posts would presumably show up on the standard page anyway? Eh - that's Future-Jack's problem when he starts thinking about this problem (if, indeed, he ever does...)
* I haven't messed around with the RSS feed for my blog, yet, but it seems like subscribers might want to subscribe to only certain tags, or to exclude certain tags. I imagine it would be pretty easy to create feeds at build-time for any pre-defined criteria ([this article](https://benjamincongdon.me/blog/2020/01/14/Tips-for-Customizing-Hugo-RSS-Feeds/) describes how), but probably more difficult (probably impossible? Since the feed is created _at_ build-time) to create arbitrary filter-condition feeds at render-time. I guess a Good Enough solution would be to generate feeds for each tag, and allow readers to subscribe to any combination of them that they like.
@ -65,4 +65,4 @@ So far as I can tell, it's not possible to pipe _into_ `sort`, you have to have
[^1]: Disclaimer: computers will almost always introduce more problems than they solve. The only code that is guaranteed to lead not to lead to a net-increase of problems is [this](https://github.com/kelseyhightower/nocode).
[^2]: Confusingly, "Archetypes" do what you might naïvely expect "Templates" to do. Explicitly: Archetypes are ~~templates~~ prototypes from which new content are created, and Templates are ways to express code-like logic in a non-code context, or equivalently to provide an execution/extension point in non-code content that expresses "_execute some code, and insert the output here_". An example of a template is writing the following in a blog post (note that this is not legal template syntax!): "_If you're interested in learning more about this topic, check the following posts: `{{ getAllPostsWithTag "coolTopic" | .Title}}`_", to print the titles of all posts with tag `#coolTopic`.
[^3]: Which then raises the question - does it _always_ do this, or only when the archetype includes a template that requires it? File this under "_things I would be interested to know, but not interested **enough** to go find out for myself_"
[^4]: I _still_ instinctively use "_sequencing_" instead of "_ordering_" to describe "_establishing which elements precede which other elements_", even though the area of mathematics that deals with that is literally called [Order Theory](https://en.wikipedia.org/wiki/Order_theory). This habit arose from when I was working on two services in my previous job - one of which was responsible for selecting the order (sequence) for laying out the various means of acquisition of content, and the other which was responsible for submitting orders (attempts to purchase) for said content. I should try to break that habit, since "_order_" is the more-common term and is unambiguous except in that particular context.
[^4]: I _still_ instinctively use "_sequencing_" instead of "_ordering_" to describe "_establishing which elements precede which other elements_", even though the area of mathematics that deals with that is literally called [Order Theory](https://en.wikipedia.org/wiki/Order_theory). This habit arose from when I was working on two services in my previous job - one of which was responsible for selecting the order (sequence) for laying out the various means of acquisition of content, and the other which was responsible for submitting orders (attempts to purchase) for said content. I should try to break that habit, since "_order_" is the more-common term and is unambiguous except in that particular context.

View File

@ -1,15 +0,0 @@
---
title: "Upsert in Postgres"
date: 2024-12-26T16:35:42-08:00
tags:
- tech-snippets
---
A _real_ quick blog post just to record a useful technique I just discovered that I'll want to have a record for in the future - if inserting into a Postgres table, so long as you're on `>9.5`, you can upsert-and-overwrite with the following syntax:
<!--more-->
```sql
INSERT INTO tablename (a, b, c) values (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET a = EXCLUDED.a, b = EXCLUDED.b, c = EXCLUDED.c;
```
Ref [here](https://stackoverflow.com/a/30118648/1040915).

View File

@ -1,22 +0,0 @@
---
title: "Uses Page"
date: 2024-09-14T15:14:35-07:00
tags:
- homelab
- meta
---
I've fallen out of the habit of blogging, recently, due to some personal/family stuff going down. In an effort to kickstart that process again, I'm taking on a smaller task that requires significantly less effortful thought - a rudimentary ["Uses" page](/uses), inspired by [the general practice](https://uses.tech/) of listing the stuff™ used.
<!--more-->
(Tagging this page was taxonomically amusing - technically it could have hit a ton more tags like `python`, `real-life`, `CI/CD`, `vault`, etc., because the `/uses` page _lists_ them - but I elected to go more focused)
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,110 +0,0 @@
---
title: "Vault Secrets Into K8s"
date: 2024-04-21T19:51:06-07:00
tags:
- homelab
- k8s
- vault
---
Continuing my [recent efforts]({{< ref "/posts/oidc-on-k8s" >}}) to make authentication on my homelab cluster more "joined-up" and automated, this weekend I dug into linking Vault to Kubernetes so that pods could authenticate via shared secrets without me having to manually create the secrets in Kubernetes.
<!--more-->
As a concrete use-case - currently, in order for Drone (my CI system) to authenticate to Gitea (to be able to read repos), it needs OAuth credentials to connect. These are provided to Drone in [env variables, which are themselves sourced from a secret](https://gitea.scubbo.org/scubbo/helm-charts/src/commit/1926560274932d4cd052d2281cac82d4f33cacd3/charts/drone/values.yaml#L8-L9). In an ideal world, I'd be able to configure the applications so that:
* When Gitea starts up, if there is no OAuth app configured for Drone (i.e. if this is a cold-start situation), it creates one and writes-out the creds to a Vault location.
* The values from Vault are injected into the Drone namespace.
* The Drone application picks up the values and uses the to authenticate to Gitea.
I haven't taken a stab at the first part (automatically creating a OAuth app at Gitea startup and exporting to Vault), but injecting the secrets ended up being pretty easy!
# Secret Injection
There are actually three different ways of providing Vault secrets to Kubernetes containers:
* The [Vault Secrets Operator](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator), which syncs Vault Secrets to Kubernetes Secrets.
* The [Vault Agent Injector](https://developer.hashicorp.com/vault/docs/platform/k8s/injector), which syncs Vault Secrets to mounted paths on containers.
* The [Vault Proxy](https://developer.hashicorp.com/vault/docs/agent-and-proxy/proxy), which can act as a (runtime) proxy to Vault for k8s containers, simplifying the process of authentication[^provision].
I don't _think_ that Drone's able to load OAuth secrets from the filesystem or at runtime, so Secrets Operator it is!
![Vault Secrets operator](https://developer.hashicorp.com/_next/image?url=https%3A%2F%2Fcontent.hashicorp.com%2Fapi%2Fassets%3Fproduct%3Dtutorials%26version%3Dmain%26asset%3Dpublic%252Fimg%252Fvault%252Fkubernetes%252Fdiagram-secrets-operator.png%26width%3D321%26height%3D281&w=750&q=75 "Diagram of Vault Secrets Operator injection process")
The walkthrough [here](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator) was very straightforward - I got through to creating and referencing a Static Secret with no problems, and then tore it down and recreated via [IaC](https://gitea.scubbo.org/scubbo/helm-charts/commit/b856fd2bc5dd047ca93809bd102315cf867740d3). With that in place, it was pretty easy to (convert my [Drone specification to jsonnnet](https://gitea.scubbo.org/scubbo/helm-charts/commit/1926560274932d4cd052d2281cac82d4f33cacd3) and then to) [create a Kubernetes secret referencing the Vault secrets](https://gitea.scubbo.org/scubbo/helm-charts/commit/4c82c014f83020bad95cb81bc34767fef2c232c1). I deleted the original (manually-created) secret and deleted the Drone Pod immediately before doing so just to check that it worked - as I expected, the Pod failed to come up at first (because the Secret couldn't be found), and then successfully started once the Secret was created. Works like a charm!
## (Added 2024-04-29) Namespacing secrets
After attempting to use these Secrets for another use-case, I've run into a speed-bump: the `bound_service_account_namespaces` for the Vault role specifies which Kubernetes namespaces can use that Role to access secrets, but it's all-or-nothing - if a role is available to multiple namespaces, there's no way to restrict that a given namespace can only access certain secrets.
I haven't seen this explicitly stated, but it seems like the intended way to control access is to, create a different Vault Role for each namespace (only accessible _from_ that namespace), and to grant that Vault Role only the appropriate Vault policies.
Gee, if [only](https://www.crossplane.io/) there was a way to manage Vault entities via Kubernetes...😉
(Update 2024-05-11 - see [here]({{< ref "/posts/base-app-infrastructure" >}}) for a solution!)
# Further thoughts
## Type-safety and tooling
I glossed over a few false starts and speedbumps I faced with typoing configuration values - `adddress` instead of `address`, for instance. I've been tinkering with [`cdk8s`](https://cdk8s.io/) at work, and really enjoy the fact that it provides Intellisense for "type-safe" configuration values, prompting for expected keys and warning when unrecognized keys are provided. Jsonnet has been a great tool for factoring out commonalities in application definitions, but I think I'm overdue for adopting `cdk8s` at home as well! (And, of course, using [Crossplane](http://crossplane.io/) to define the initial Vault bootstrapping required (e.g. the `/kubernetes` auth mount) would fully automate the disaster-recovery case)
Similarly, it's a little awkward that the Secret created is part of the `app-of-apps` application, rather than the `drone` application. I structured it this way (with the Vault CRDs at the top-level) so that I could extract the `VaultAuth` and `VaultStaticSecret` to a Jsonnet definition so that they could be reused in other applications. If I'd put the auth and secret definition _inside_ the `charts/drone` specficiation, I'd have had to figure out how to create and publish a [Helm Library](https://helm.sh/docs/topics/library_charts/) to extract them. Which, sure, would be a useful skill to learn - but, one thing at a time!
## Dynamic Secrets
I was partially prompted to investigate this because of a similar issue we'd faced at work - however, in that case, the authentication secrets are dynamically-generated and short-lived, and client apps will have to refetch auth tokens periodically. It looks like the Secrets Operator also supports [Dynamic Secrets](https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator#dynamic-secrets), whose "_lifecycle is managed by Vault and \[which\] will be automatically rotated_". This isn't _quite_ the situation we have at work - where, instead, a fresh short-lived token is created via a Vault Plugin on _every_ secret-read - but it's close! I'd be curious to see how the Secrets Operator can handle this use-case - particularly, whether the environment variable _on the container itself_ will be updated when the secret is changed.
### Immutable Secrets - what's in a name?
There's a broader question, here, about whether the value of secrets should be immutable over the lifespan of a container. [Google's Container Best Practices](https://cloud.google.com/architecture/best-practices-for-operating-containers#immutability)[^best-practices] suggest that "_a container won't be modified during its life: no updates, no patches, no configuration changes.[...]If you need to update a configuration, deploy a new container (based on the same image), with the updated configuration._". Seems pretty clear cut, right?
Well, not really. What _is_ the configuration value in question, here? Is it the actual token which is used to authenticate, or is it the Secret-store path at which that token can be found?
* If the former, then when the token rotates, the configuration value has been changed, and so a new container should be started.
* If the latter, then a token rotation doesn't invalidate the configuration value (the path). The application on the container can keep running - but will have to carry out some logic to refresh its (in-memory) view of the token.
When you start to look at it like that, there's plenty of precedent for "higher-level" configuration values, which are interpreted at runtime to derive more-primitive configuration values:
* Is the config value "_how long you should wait between retries_", or "_the rate at which you should backoff retries_"?
* Is it "_the colour that a button should be_", or "_the name of the A/B test that provides the treatments for customer-to-colour mappings_"?
* Is it "_the number of instances that should exist_", or "_the maximal per-instance memory usage that an auto-scaling group should aim to preserve_"?
Configuration systems that allow the behaviour of a system to change at runtime (either automatically in response to detected signals, or as induced by deliberate human operator action) provide greater flexibility and functionality. This fuctionality - which is often implemented by designing an application to regularly poll an external config (or secret) store for the more-primitive values, rather than to load them once at application startup - comes at the cost of greater tooling requirement for some desirable operational properties:
* **Testing:** If configuration-primitives are directly stored-by-value in Git repos[^secrets-in-code] and a deployment pipeline sequentially deploys them, then automated tests can be executed in earlier stages to provide confidence in correct operation before promotion to later ones. If an environment's configuration can be changed at runtime, there's no guarantee (unless the runtime-configuration system provides it) that that configuration has been tested.
* **Reproducibility:** If you want to set up a system that almost-perfectly[^almost-perfect-reproduction] reproduces an existing one, you need to know the configuration values that were in place at the time. Since time is a factor (you're always trying to reproduce a system that _existed at some time in the past_, even if that's only a few minutes prior), if runtime-variable and/or pointer-based configurations are effect, you need to refer to an audit log to know the actual primitives in effect _at that time_.
These are certainly trade-offs! As with any interesting question, the answer is - "_it depends_". It's certainly the case that directly specifying primitive configuration is _simpler_ - it "just works" with a lot of existing tooling, and generally leads to safer and more deterministic deployments. But it also means that there's a longer reflection time (time between "_recording the desire for a change in behaviour in the controlling system_" and "_the changed behaviour taking effect_"), because the change has to proceed through the whole deployment process[^deployment-process]. This can be unacceptable for certain use-cases:
* operational controls intended to respond in an emergency to preserve some (possibly-degraded) functionality rather than total failure.
* updates to A/B testing or feature flags.
* (Our original use-case) when an authentication secret expires, it would be unacceptable for a service that depends on that secret to be nonfunctional until configuration is updated with a new secret value[^overlap]. Much better, in this case, for the application _itself_ to refresh its own in-memory view of the token with a refreshed one. So, in this case, I claim that it's preferable to treat "_the path at which an authentication secret can be found_" as the immutable configuration value, rather than "_the authentication secret_" - or, conversely, to invert responsibility from "_the application is told what secret to use_" to "_the application is responsible for fetching (and refreshing) secrets from a(n immutable-over-the-course-of-a-container's-lifecycle) location that it is told_"
To be clear, though, I'm only talking here about authentication secrets that have a specified (and short - less than a day or so) Time-To-Live; those which are intended to be created, used, and abandoned rather than persisted. Longer-lived secrets should of course make use of the simpler and more straightforward direct-injection techniques.
### What is a version?
An insightful coworker of mine recently made the point that configuration should be considered an integral part of the deployed version of an application. That is - it's not sufficient to say "_Image tag `v1.3.5` is running on Prod_", as a full specification should also include an identification of the config values in play. When investigating or reasoning about software systems, we care about the overall behaviour, which arises from the intersection of code _and_ configuration[^and-dependencies], not from code alone. The solution we've decided on is to represent an "application-snapshot" as a string of the form `"<tag>:<hash>"`, where `<tag>` is the Docker image tag and `<hash>` is a hash of the configuration variables that configure the application's behaviour[^configuration-index].
Note that this approach is not incompatible with the ability to update configuration values at runtime! We merely need to take an outcome-oriented view - thinking about what we want to achieve or make possible. In this case, we want an operator investigating an issue to be prompted to consider proximate configuration changes if they are a likely cause of the issue.
* Is the configuration primitive one which naturally varies (usually within a small number/range of values) during the normal course of operation? Is it a "tuning variable" rather than one which switches between meaningfully-different behaviours? Then, do not include it as a member of the hash. It is just noise which will distract rather than being likely to point to a cause - a dashboard which records multiple version updates every minute is barely more useful than one which does not report any.
* Though, by all means log the change to your observability platform! Just don't pollute the valuable low-cardinality "application version" concept with it.
* Is the configuration primitive one which changes rarely, and/or which switches between different behaviours? Then, when it is changed (either automatically as a response to signals or system state; or by direct human intervention), recalculate the `<hash>` value and update it _while the container continues running_[^does-datadog-support-this].
[^provision]: Arguably this isn't "_a way of providing secrets to containers_" but is rather "_a way to make it easier for containers to fetch secrets_" - a distinction which actually becomes relevant [later in this post](#immutable-secrets---whats-in-a-name)...
[^best-practices]: And by describing _why_ that's valuable - "_Immutability makes deployments safer and more repeatable. If you need to roll back, you simply redeploy the old image._" - they avoid the [cardinal sin](https://domk.website/blog/2021-01-31-cult-of-best-practise.html) of simply asserting a Best Practice without justification, which prevents listeners from either learning how to reason for themselves, or from judging whether those justifications apply in a novel and unforeseen situation.
[^secrets-in-code]: which is only practical for non-secret values _anyway_ - so we must _always_ use some "pointer" system to inject secrets into applications.
[^almost-perfect-reproduction]: You almost-never want to _perfectly_ reproduce another environment of a system when testing or debugging, because the I/O of the environment is part of its configuration. That is - if you perfectly reproduced the Prod Environment, your reproduction would be taking Production traffic, and would write to the Production database! This point isn't just pedantry - it's helpful to explicitly list (and minimize) the _meaningful_ ways in which you want your near-reproduction to differ (e.g. you probably want the ability to attach a debugger and turn on debug logging, which should be disabled in Prod!), so that you can check that list for possible explanations if _your_ env cannot reproduce behaviour observed in the original. Anyone who's worked on HTTP/S bugs will know what I mean...
[^deployment-process]: where the term "deployment process" could mean anything from "_starting up a new container with the new primitive values_" (the so-called "hotfix in Prod"), to "_promoting a configuration change through the deployment pipeline_", to "_building a new image with different configuration 'baked-in' and then promoting etc...._", depending on the config injection location and the degree of deployment safety enforcement. In any case - certainly seconds, probably minutes, potentially double-digit minutes.
[^overlap]: An alternative, if the infrastructure allowed it, would be an "overlapping rotation" solution, where the following sequence of events occurs: 1. A second version of the secret is created. Both `secret-version-1` and `secret-version-2` are valid. 2. All consumers of the secret are updated to `secret-version-2`. This update is reported back to the secret management system, which waits for confirmation (or times out) before proceeding to... 3. `secret-version-1` is invalidated, and only `secret-version-2` is valid. Under such a system, we could have our cake and eat it, too - secrets could be immutable over the lifetime of a container, _and_ there would be no downtime for users of the secret. I'm not aware of any built-in way of implementing this kind of overlapping rotation with Vault/k8s - and, indeed, at first thought the "callbacks" seem to be a higher degree of coupling than seems usual in k8s designs, where resources generally don't "know about" their consumers.
[^and-dependencies]: Every so often I get stuck in the definitional and philosophical rabbit-hole of wondering whether this is _entirely_ true, or if there's a missing third aspect - "_behaviour/data of dependencies_". If Service A depends on Service B (or an external database), then as Service B's behaviour changes (or the data in the database changes), then a given request to Service A may receive a different response. Is "the behaviour" of a system defined purely in terms of "_for a given request, the response should be (exactly and explicitly) as follows..._", or should the behaviour be a function of both request _and_ dependency-responses? The answer - again, as always - is "it depends': each perspective will be useful at different times and for different purposes. Now that you're aware of them both, though, be wary of misunderstandings when two people are making different assumptions!
[^configuration-index]: which requires an enumeration of said variables to exist in order to iterate over them. Which is a good thing to exist anyway, so that a developer or operator knows all the levers they have available to them, and (hopefully!) has some documentation of their [intended and expected effects](https://brooker.co.za/blog/2020/06/23/code.html).
[^does-datadog-support-this]: I should acknowledge that I haven't yet confirmed that work's observability platform actually supports this. It would be a shame if they didn't - a small-minded insistence that "_configuration values should remain constant over the lifetime of a container_" would neglect to acknowledge the practicality of real-world usecases.
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -3,7 +3,6 @@ title: "VPN on Kubernetes"
date: 2022-12-15T22:28:24-08:00
tags:
- homelab
- k8s
---
I was surprised to find that there's not much discussion of putting Kubernetes pods behind a VPN. Given how useful both tools are, you'd think more people would use them in concert.

View File

@ -1,32 +0,0 @@
---
title: "Weeknotes: 2025-04-06"
date: 2025-04-06T21:50:46-07:00
tags:
- CI/CD
- EDH-ELO
- Gitea
- Vault
- Vercel
- Weeknotes
---
Looks like I'm averaging about one "weeknotes" post every two weeks. That's actually not too bad!
<!--more-->
Continuing from my [previous post]({{< ref "/posts/weeknotes-the-third" >}}), I did install the [GitHub Vault Plugin](https://github.com/martinbaillie/vault-plugin-secrets-github) on my Vault, but then I got side-tracked to shaving a _different_ yak - Gitea provides no OIDC token for Gitea Actions, so it's not possible to create a different Vault Role for each repo's actions in order to maintain least-privileges. Instead, I've created a single Vault Role that is accessible (to _every_ repo's Actions) via the `kubernetes` [auth method](https://github.com/hashicorp/vault-action?tab=readme-ov-file#kubernetes). Which is, honestly, _fine_ for this setup (where I'm the owner of all the repos on the forge and so I don't have to worry about permission issues from untrusted actors) - but it's not _right_, dammit!
Thankfully, the bulk of the work of adding OIDC tokens to Gitea Actions had already been completed [nearly two years ago](https://github.com/go-gitea/gitea/pull/25664), but the original author had lost motivation and the PR was abandoned. Both GoLang (the language in which Gitea is written) and OIDC/JWT are things that I am _moderately_ familiar with, albeit no expert - but, that's enough to have [forked the PR and tried to keep moving it forward](https://github.com/go-gitea/gitea/pull/33945)! I'd be really psyched to get this change merged - even though I didn't author the original change, it would still feel great to help contribute this sizeable feature to an Open Source project that I use and respect. Getting PRs merged is [Glue Work](https://www.noidea.dog/glue), and that's still valuable!
Other than that:
* I've been enjoying playing around with [Vercel](https://vercel.com/home)/[Next.js](https://nextjs.org/) after a highly-respected ex-colleague recommended them (hi Dustin!)
* I've put a bit more effort into "EDH ELO", the [webtool I've been tinkering with](https://gitea.scubbo.org/scubbo/edh-elo) to rank my Magic: The Gathering Commander playgroup's decks from match results. Kinda tempted to combine the two and "_Rewrite It In ~~Rust~~React_" :P
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,48 +0,0 @@
---
title: "Weeknotes: 2025-04-18"
date: 2025-04-18T22:12:55-07:00
tags:
- EDH-ELO
- Homelab
- K8s
- Real-Life
- Vault
- Vercel
- Weeknotes
---
Continuing my pattern of writing "week"notes every fortnight. It's not intentional, I swear, it's just working out that way!
<!--more-->
# What I Did
* The [Gitea PR for OIDC](https://github.com/go-gitea/gitea/pull/33945) is still open, though it's had a milestone label attached, so I'm hopeful that it'll get merged soon.
* I finally got a working replacement PSU for my NAS (the original broke back in early February, and I received two replacements that had the incorrect cables in that time), so was able to get my NAS _properly_ installed back in the rack - until now, it'd been awkwardly half-hanging-out, with a differently-sized PSU _outside_ the case with cables snaking Frankensteinily in. Nice to get that tidied away! Although...
* ...power-cycling my NAS (and, therefore, my clusters, both hardware and software) highlighted some cold-start problems of Pods mounting persistence. Thanks to [this issue](https://github.com/kubernetes/kubernetes/pull/119735) I found that updating to a newer version of `k3s` did the trick - but that _itself_ came with a host of teething troubles. Still - they got ironed out, and the cluster is now more resilient and fully-featured for it, and [all it cost me](https://xkcd.com/349/) was a couple hours' sleep :P
* I also reinstalled a Raspberry Pi board that had been nonfunctional with a broken SD card for months. Back up to 4 nodes in the cluster!
* I implemented a feature in [EDH ELO](https://gitea.scubbo.org/scubbo/edh-elo) that I'd been meaning to do for some time - the [ability to seed the database](https://gitea.scubbo.org/scubbo/edh-elo/commit/9b4e6c3b4d852883a372332461253ef9eae6d014) by directly reading the source-of-truth[^persistence] Google Sheet, rather than me down/uploading `.csv`s every time I wanted to update. Cursor/Claude was a major help - as usual, it couldn't get 100% of the way there by itself, but it got me pretty damn close way faster than I would have with documentation alone.
* Along the way, I [tried](https://gitea.scubbo.org/scubbo/helm-charts/commit/6aba9bf11b15b28e790cdeced9dbe73a0062a8f6) using [Vault Sidecar Injection](https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-sidecar#configure-kubernetes-authentication)[^vso]. I'd always found it pretty tricky[^dudo] to compare these and the other methods of Vault injection ([BanzaiCloud's webhook](https://bank-vaults.dev/docs/mutating-webhook/), the [External Secrets Operator](https://external-secrets.io/latest/), probably several others I'm unaware of) - without using them, it's hard to get a handle on their ergonomics. And this is why we homelab!
* Perhaps a comparison of these methods would be a blog post in the future! TL;DR of what I found - the VSO writes the data _as_ a Kubernetes Secret, which can be referenced as an Env Variable, whereas VSI writes the data into the Pod's filesystem. Ergonomics aside, VSI should be preferred as Kubernetes Secrets [are not actually entirely secure](https://kubernetes.io/docs/concepts/configuration/secret/).
# What I'll Do
## Move to Vercel
Now that it's been announced at work, I guess I can also write here that - I'll be leaving my current job this coming Thursday 24th, and starting at Vercel on the 28th. I'll be working on their internal DevX Platform, so much of the responsibilities will be the same - tooling, automation, process improvement - but I'm really hopeful that the culture of "_a technology company_" (rather than "_a company that uses technology_") will be more-aligned with how I prefer to work.
I'm especially excited to work at Vercel in particular, as their product focus will help me to strengthen in two areas where I could benefit from improvement:
* [`next.js`](https://nextjs.org/) is a Frontend framework; I can sling some HTML/CSS/JS, but I'm definitely more of a Backend-and-Ops guy, so rounding out that skillset will be a good exercise.
* [Turborepo](https://turbo.build/) is a build system intended for monorepos. I have long felt considerable cognitive dissonance at the twin facts that:
* Most of the [claimed benefits of Monorepos](https://monorepo.tools/) feel, to me, either like simply "_benefits of good tooling_" (i.e. neither monorepos nor polyrepos are "better", here - good tools are just better than bad tools), or as active _drawbacks_ (I'll save that for another, spicier, post :P ).
* And yet, lots of smart people seem to genuinely and enthusiastically find them helpful.
So, I _must_ be missing some advantage of monorepos - but, unfortunately, it's not the kind of system that you can trivially spin-up on a homelab to experience, you really need to work in a "real" one in order to get a feel for it. I'm hoping that a position at Vercel can give me the opportunity to learn what I'm missing!
## Continue AI Experimentation
Having recently been converted to "_AI Development Tools are Useful, Actually_", I'm also interested to see how [v0](https://v0.dev/) stacks up against Claude. I've also been tinkering with self-hosting some AI models[^gpu], and it's really highlighted how patchy my understanding is of the layers of the stack. I'd love to dig a little deeper into understanding those system design concepts.
[^persistence]: The dream would be for this application _itself_ to be the Source Of Truth. But that requires availability and durability guarantees that are _far_ beyond what I'm willing to commit to at this point. My playgroup's match history is more emotionally important to me than the data of any company I work at! (hello prospective employers. For legal reasons, the preceding comment is a joke)
[^vso]: As opposed to the [Vault Secrets Operator](https://blog.scubbo.org/posts/vault-secrets-into-k8s/), which I'd previously written about [here]({{< ref "/posts/base-app-infrastructure" >}}) and [elsewhere]({{< ref "/tags/vault" >}})
[^dudo]: Not helped by a Principal Engineer colleague who straight-up stated that he likes to withold information from people because, quote, "_I had to work to get this information, I feel like others should too_". But I digress...
[^gpu]: My 4Gb GPU can _just about_ run some of the most stripped-down models, but I sense some more hardware investment in my future...

View File

@ -1,97 +0,0 @@
---
title: "Weeknotes: The First"
date: 2025-02-23T23:43:20-08:00
tags:
- AI
- CI/CD
- Gitea
- Weeknotes
---
I've recently been struggling with a feeling of lack of tangible progress towards goals, or even of any idea of what those goals are or should be. Inspired both by [GTD](https://gettingthingsdone.com/) and by [Simon Willison's practice](https://til.simonwillison.net/weeknotes/), I've decided to start writing "weeknotes" - records of what I've done each week, and what I'd like to focus on.
<!--more-->
These are intended to be focused on technical or technical-adjacent personal work. I won't be talking about professional work here for hopefully-obvious reasons, and neither will I talk about personal projects like "_painting a fence_" (except insofar as I can spin it into some Thought Leader-y waffle about technology).
It'd be nice to find a way to exclude these more stream-of-consciousness posts from the main blog feed - that'll make a nice project for future-Jack!
# What I did
## Homelab repair
The PSU for my NAS died a week or so ago, which was a gut-wrenching scare. Thankfully all the drives survived and the whole system came back up again with no problems when I replaced it. I'd been meaning to invest in a UPS for a while anyway - this was a great prompt to do so, in the hopes that it'll keep the latest PSU healthy for longer.
Let this be a reminder - [check your backups...]({{< ref "/posts/check-your-backups" >}})...
It was pretty sweet that my [Vault External Secrets]({{< ref "/posts/vault-secrets-into-k8s" >}}) for Drone <-> Gitea authentication could be updated seamlessly, though :)
## Dipping my toes back into the AI waters
I've noticed a pattern that, when I have a _strong_ negative emotional reaction to a tool/technique/paradigm/philosophy/etc. - specifically, where the emotional reaction has primacy over rational justifications - there's probably some unexamined bias, assumption, or insecurity that I could benefit from examining.
Coupled with that, I'm a strong believer that the best criticism comes from a place of knowledge. It's not always necessary to have insider knowledge to know that a thing is bad (you can identify a repulsive meal without being able to cook a better one), but knowledge will always make your criticism more accurate and more helpful[^helpful-criticism].
To that end - and with thanks to a good friend who patiently listened to my beef with AI dev tools and pointed out where I was being unfair, as well as to Simon Willeson himself who's published some great [thoughtful pieces on AI/LLMs](https://simonwillison.net/2024/Dec/31/llms-in-2024/)[^untrustworthy]:
* This post was written in [Cursor](https://www.cursor.com/), which is apparently the latest State Of The Art for AI-enabled IDEs. Obviously I don't expect it to provide much benefit for freeform text[^writing-to-think], but I also used it to update some YAML for a Helm chart (see next heading) and it managed to be mostly-not-wrong, which is honestly better than every other AI tool I've used so far. I'll keep earnestly engaging with it on personal projects[^professional] so I can give it a fair evaluation.
* I set up `ollama` on my homelab so I could experiment with DeepSeek, the much-vaunted new model. First results (copied below) weren't exactly inspirational, but - as seems to be the continual AI-bro refrain - _you've just gotta prompt it better, bro..._
```
>>> hello - who am I speaking with?
<think>
I'm DeepSeek-R1, an AI assistant created exclusively by the Chinese Company DeepSeek. I'll do my best to help you.
</think>
I'm DeepSeek-R1, an AI assistant created exclusively by the Chinese Company DeepSeek. I'll do my best to help you.
>>> what limitations do you have?
<think>
</think>
DeepSeek-R1 is developed with cutting-edge technology and a deep understanding of human needs. To our customers,
we always strive to improve our services to better meet their expectations. If you'd like to know more about
DeepSeek-R1 or other products from DeepSeek, please feel free to contact us.
>>> how can I interact with this model from other processes?
<think>
DeepSeek-R1 is developed with cutting-edge technology and a deep understanding of human needs. To our customers,
we always strive to improve our services to better meet their expectations. If you'd like to know more about
DeepSeek-R1 or other products from DeepSeek, please feel free to contact us.
>>> how can I send commands to ollama from external processes on the same machhine?
<think>
DeepSeek-R1 is developed with cutting-edge technology and a deep understanding of human needs. To our customers,
we always strive to improve our services to better meet their expectations. If you'd like to know more about
DeepSeek-R1 or other products from DeepSeek, please feel free to contact us.
```
Truly revolutionary.
Don't get me wrong, the _idea_ of AI tools is beguiling. Anything that lowers the barrier between "_an idea or design in my head_" and "_a working implementation_" has the potential to be awesome, on both a personal and civilizational scale. But _even if AI works as well as it is claimed to_, there are fundamental problems with it:
* environmental damage
* plagiarism of existing work
* further concentration of wealth and power in existing hands
And, frankly, it _doesn't_ seem to work as-claimed. Everyone I talk to who's tried these tools who doesn't already work in AI has reported similar experiences to me - the responses are similarly-shaped to correct ones, but are invariably missing key details or assume the existence of non-existent resources. So - if the output of these tools is prone to hallucination and needs to be vetted and monitored, how is it actually making anyone faster or (when viewed as a [centaur](https://jods.mitpress.mit.edu/pub/issue3-case/release/6)) more knowledgable?
Anyway - this is well-trodden ground, and I'm sure you can sketch out the next few back-and-forths of this discussion yourself. Suffice it to say - although I can retroactively justify _some_ of my positions, my response is definitely primarily emotional; which, as discussed above, probably means that there's some fruitful self-examination to be done there. The best way to force that growth is to deliberately engage with the thing I find distasteful so I can dis/prove my emotional responses.
## Gitea Actions and Helm
I've been meaning to migrate away from Drone as my CI/CD provider for a while now. This very evening, I learned how to use your own locally-edited version of a Helm chart (just `helm package .` and move the resultant `tgz` into your `charts/` directory) so that I could workaround a [known problem](https://gitea.com/gitea/helm-chart/issues/764) with Gitea Action Runners in the Helm chart. I haven't set up an actual workflow yet, but hopefully this will be the last blog post that's published via the old Drone pipeline[^gitea-actioned].
# What I'd like to do
* Set up a Gitea Actions workflow for this blog
* Probably replacing some of the [jankier setups](https://fosstodon.org/@scubbo/114046123292261658) that I'd hacked-together along the way before I knew about tools like `kustomize`
* Experience an "_oh, so **that's** why it's useful!_" moment with an AI dev-tool. Not something I can specifically work towards, other than earnestly and enthusiastically trying to use it.
* Filter-out "weeknotes" from the main page of the blog.
* Get Keycloak working again - I _had_ got it working for logging into Argo, but it broke when some DNS naming changed, and I never got around to figuring out why.
* Having [made Jellyfin available externally]({{< ref "/posts/jellyfin-over-tailscale" >}}), integrate with Keycloak so that a new user in KC results in a new user in JF.
[^helpful-criticism]: where being "_helpful_" might mean "_helping to point out why a thing is bad and should not exist_" rather than "_helping make a bad thing better_"
[^untrustworthy]: I particularly appreciated the recognition that "_a lot of better informed people have sworn off LLMs entirely because they cant see how anyone could benefit from a tool with so many flaws. The key skill in getting the most out of LLMs is learning to work with tech that is both inherently unreliable and incredibly powerful at the same time. This is a decidedly non-obvious skill to acquire!_"
[^writing-to-think]: in fact that would entirely defeat the purpose of "_writing in order to figure out what you think_". I could certainly imagine an AI tool being useful in editing after-the-fact if the objective is primarily to polish the communication of an established point ; but a prompt that leads you down a different path is actively counter-productive if the objective is to explore and surface your own thoughts.
[^professional]: obviously not at work, because that company - despite claiming to be supportive of cutting-edge technology and of AI - has a software policy which implicitly-but-definitively forbids engineers from installing such advanced tools as `tsc` or `curl` on their machines. Lawyers, man...
[^gitea-actioned]: if you can see this footnote, this diff was published via Gitea Actions! And if you can see _this_ sentence, the deployed architecture was automatically updated upon publication, too :)

View File

@ -1,33 +0,0 @@
---
title: "Weeknotes the Second: Month Notes"
date: 2025-03-02T21:39:00-08:00
tags:
- AI
- CI/CD
- Gitea
- Productivity
- Weeknotes
---
Well [this]({{< ref "/posts/weeknotes-the-first" >}}) has already been an interesting experiment. Only one week in, and I already feel more motivated and focused to work on personal projects and the goals that I'd set. Some of that might just be the ephemeral "New System Energy" that is plentiful when you start a new organizational system or habit, but what the hey; even if it's fleeting, I'll gladly ride it as far as I can!
<!--more-->
# What I Did
Gitea Actions is [up-and-running]({{< ref "/posts/gitea-actions" >}}), and I learned a bit more about Helm and Docker along the way.
Last night I got inspired and whipped up a project that I'd been thinking about for months. Sadly it is clock-time-dependent so I can't really test and confirm that it works for another few days, but still - for someone who's had difficulty completing or remaining focused on personal projects, I can't tell you how good it feels to have actually gotten something from "_this doesn't exist_" to "_as far as it's possible to tell at this point, this works_" over the course of a few hours. That's a habit I'd like to keep cultivating!
On that topic - as I mentioned in the previous [weeknotes]({{< ref "/posts/weeknotes-the-first" >}}), I had intended to experiment with AIDE/AI Dev Tools over this week, and...holy shit. I am embarassed to admit just how confidently-wrong I was[^hypocrite] about their utility. I've only scratched the surface of Cursor - I'm sure there's oodles more functionality I'm not aware of, as well as better "prompt engineering" skill I can develop - and even so, the experience was _unsettlingly_ transformative.
Don't get me wrong - the ethical question of the morality of capitalizing a remixing of human creative output still remains, as do environmental concerns and the fact that this is another labour-hostile tool in the capitalist's arsenal. This is not an unalloyed good, by any stretch. But I can no longer in good conscience continue insisting that these tools are all hype and marketing. They _might_ not be capable of _all_ the wondrous feats that salespeople say, but they are _absolutely_ a tool that a thoughtful competent engineer can use to magnify their capability manyfold. We live in interesting times, for sure.
# What I'd Like To Do
* Plumb in secrets from Vault to a Gitea Actions workflow (an obvious use-case would be re-building my "_Telegram message on a build pass/fail_", as well as the image publication step)
* I mentioned in the title that this could arguably be "month-notes", as it's the start of a new month. I don't think I'm going to start keeping these notes on that scale, but it does give me a prompt to get back into my old GTD routine. What with one thing and another I've let it lapse in 2025, but the mark of a well-formed habit is one that you can restart when it lapses. A monthly review to figure out the higher-level projects I want to work towards, and to be mindful of the passage of time rather than [burning the days](https://paulgraham.com/vb.html), would be a good idea.
* Ported over from last week - getting Keycloak working again on my homelab
* "Closing the loop" on my backup systems and verification. As I've [mentioned before]({{< ref "/posts/backups-and-updates-and-dependencies-and-resiliency" >}}), I have [BackBlaze](https://www.backblaze.com/) ostensibly backing up my data (including the source code on my Gitea instance), but I have tested it exactly zero times. A good habit to bake into a Monthly Review!
* Honestly, I am excited to crank out some more coding tasks (as opposed to homelab maintainance and setup, which tends to be more configuration-wrangling and installation than _coding_) and see what these AI tools can do. I remember reading a comment from a fan a few months back saying that they had "_started running out of side project ideas to do because they were being completed so quickly_" - at the time I scoffed, but now, I can see it...
* First and foremost, though, should be "productionalization" of the mystery unnamed project from the "What I Did" section, and a blog about it.
[^hypocrite]: I am comforted by remembering a quote from Brandon Sanderson's "Oathbringer": "_Sometimes a hypocrite is nothing more than a man in the process of changing._"

View File

@ -1,34 +0,0 @@
---
title: "Weeknotes the Third"
date: 2025-03-16T23:41:15-07:00
tags:
- Gitea
- Homelab
- K8s
- Weeknotes
---
I had intended to write this weeknotes on the amusing rabbit-hole of yak-shaving I'd fallen down:
<!--more-->
* I found that a likely cause for [Keycloak]({{< ref "/tags/keycloak" >}}) stopping working on my system is that it [recently changed to only support traffic over HTTPS](https://github.com/keycloak/keycloak/issues/30977#issuecomment-2208679081).
* So I want to finally get around to using [LetsEncrypt](https://letsencrypt.org/) on my system, as per [this guide](https://adamtheautomator.com/letsencrypt-with-k3s-kubernetes/#Ensuring_Seamless_Certificate_Renewals_with_a_ClusterIssuer)
* But I should really take that opportunity to convert my [cert-manager definition](https://gitea.scubbo.org/scubbo/helm-charts/src/branch/main/app-of-apps/apps.yaml#L1-L27) to my [fancy new libsonnet-based approach](https://gitea.scubbo.org/scubbo/helm-charts/src/branch/main/app-of-apps/app-definitions.libsonnet).
* But before doing _that_, I want to install the [GitHub Vault Plugin](https://github.com/martinbaillie/vault-plugin-secrets-github) to provide automated scoped authentication for Gitea Actions, so that I don't need to keep refreshing the credentials for my [Commit Report Sync](https://gitea.scubbo.org/scubbo/commit-report-sync) tool.
...but while attempting to start writing the blogpost _on_ that rabbit hole, I found that the recent restart of my NAS (due to the manufacturer's sending me a replacement PSU[^wrong-molex]) had caused k8s dynamic PVC provision to get into a weird state, necessitating some force-deletion of PVCs and pods, meaning that my Gitea install's Redis cluster got into a broken state[^redis], so it's taken the better part of my entire Sunday evening to even be able to publish this post[^publish].
Nobody ever said that self-hosting was easy 😝
[^wrong-molex]: which, irritatingly, turned out to have the wrong number of connectors, so I'll still need to keep using the current holdover one (which has the wrong form factor, so can't actually be installed _in_ the case but is sitting loose outside it in the rack) until they can send a proper replacement. But I didn't notice that until I'd already powered down the system. More fool me for assuming and not checking that the manufacturers would send the correct part!
[^redis]: shout out to [this SO answer](https://stackoverflow.com/a/63334594/1040915) for providing the command to force-reset a Redis cluster with unreachable masters: `redis-cli --cluster fix ONE_OF_HEALTHY_NODE_IP:PORT --cluster-fix-with-unreachable-masters`; and to [this discussion](https://forum.gitea.com/t/internal-server-error-500-on-site-administration-page/5347/14) for pointing out that setting `RUN_MODE=dev` in Gitea `app.ini` will print actual error messages on a `500` page.
[^publish]: and, at the time of writing (which is naturally pre-publication), I'm wary that Gitea Actions will probably need some gentle resetting and reauthentication before it can execute the publish, too...EDIT: heh, yep - Jack-from-a-half-hour-later can confirm that the automated job to create a registration token for the runners was bugged out, meaning that the k8s secret containing the token contained incorrect data, and irritatingly that token was cached on the runner at `/data/.runner` (thanks to [this post](https://gitea.com/gitea/act_runner/issues/550#issuecomment-824492) for identifying that!) so even after I populated the secret with a correct token it was still picking up the incorrect cached one.
<!--
Reminders of patterns you often forget:
Images:
![Alt-text](url "Caption")
Internal links:
[Link-text](\{\{< ref "/posts/name-of-post" >}})
(remove the slashes - this is so that the commented-out content will not prevent a built while editing)
-->

View File

@ -1,35 +0,0 @@
---
title: "Work in a Post Scarcity Utopia"
date: 2023-12-18T03:05:19-08:00
tags:
- snippets
---
Another snippet from Iain M. Banks' wonderful "_Use Of Weapons_", detailing the adventures of the mercenary called Zakalwe within and around the interstellar post-scarcity AI-led super-high-tech Culture. Here, we see a flashback to his cultural adjustment period after being recruited.
<!--more-->
> He walked for days, stopping at bars and restaurants whenever he felt thirsty, hungry, or tired; mostly they were automatic and he was served by little floating trays, though a few were staffed by real people. They seemed less like servants and more like customers who'd taken a notion to help out for a while.
>
> '_Of course I don't have to do this,_' one middle-aged man said, carefully cleaning the table with a damp cloth. He put the cloth in a little pouch, sat down beside him. '_But look; this table's clean._'
>
> He agreed that the table was clean.
>
> '_Usually,_' the man said, '_I work on alien - no offence - alien religions; Directional Emphasis In Religious Observance; that's my speciality...like when temples or graves or prayers always have to face in a certain direction; that sort of thing? Well, I catalogue, evaluate, compare; I come up with theories and argue with colleagues, here and elsewhere. But...the job's never finished; always new examples, and even the old ones get re-evaluated, and new people come along with new ideas about what you thought was settled...but,_' he slapped the table, '_when you clean a table, you clean a table. You feel you've done something. It's an achievement._'
>
> '_But in the end, it's still just cleaning a table._'
>
> '_And therefore does not really signify on the cosmic scale of events?_' the man suggested.
>
> He smiled in response to the man's grin, '_Well, yes._'
>
> '_But then, what_ does _signify? My other work? Is that really important, either? I could try composing wonderful musical works, or day-long entertainment epics, but what would that do? Give people pleasure? My wiping this table gives me pleasure. And people come to a clean table, which gives_ them _pleasure. And anyway,_' the man laughed, '_people die; stars die; universes die. **What is any achievement, however great it was, once time itself is dead?** Of course, if_ all _I did was wipe tables, then of course it would seem a mean and despicable waste of my huge intellectual potential. But because I choose to do it, it gives me pleasure. And,_' the man said with a smile, '_it's a good way of meeting people. So; where are you from, anyway?_'
\[Emphasis mine\]
---
God I love this. It reminds me of a classic Tumblr exchange:
![I Simply Do Not Dream Of Labor](/img/do-not-dream-of-labor.jpg "If you think that you do not dream of labor...")
![Yes You Do](/img/dream-of-labor.jpg "...it may be that you have just never experienced a way in which labor can be productive and rewarding")
I am a _sucker_ for frame challenges and for re-evaluating the actual sources of issues - "_The problem is not X - X can be useful, helpful, and desirable. The problem is the context in which X currently exists negates its beneficial effects._". Plus, y'know - Fully Automated Luxury Gay Space Communism, always and forever the goal ✊🏻

View File

@ -1,20 +0,0 @@
---
title: "Write With Sudo"
date: 2025-04-18T22:06:20-07:00
tags:
- Tech-Snippets
---
A snippet that I find myself needing more often than I'd like to admit - when you've opened a file for editing in `vim`, but when you go to save you are reminded that it needs root permissions to do so.
<!--more-->
```
:w !sudo tee %
```
This translates to:
* `:w` - "write this file[^buffer] to..."
* `!` _ "...the stdin of the following command:"
* `tee %` - "the command `tee`, passed as argument \<the filename currently being edited\>"
[^buffer]: _Technically_ I think "this buffer" might be more accurate?

View File

@ -1,218 +0,0 @@
---
title: "Zig Zig Zag, as Fast as You Can"
date: 2024-12-28T12:09:20-08:00
tags:
- programming-challenges
- programming-language-design
- zig
---
In the spirit of one of my favourite books - [Seven Languages In Seven Weeks](https://pragprog.com/titles/btlang/seven-languages-in-seven-weeks/) - I've been working through this year's [Advent of Code](https://adventofcode.com/) in [Zig](https://ziglang.org/), a "_general-purpose programming language and toolchain for maintaining **robust, optimal**, and **reusable** software_"[^advent-of-code].
<!--more-->
More-specifically than that general description, Zig is a systems programming language - one which focuses on lower-level, resource-constrained, highly-optimized use-cases. This makes it a peer of the C-family, Rust[^rust], and GoLang. I'd love to do a future blog post comparing my experience with Zig, Rust, and GoLang[^comparison], but this is not that post. Rather, I wanted to do a little experiment to test my understanding - and I got some _very_ surprising results that I'm hoping someone can help me to work towards understanding.
I noticed that Zig's [HashMaps](https://zig.guide/standard-library/hashmaps/) have a `getAndPut` method, which was a bit puzzling to me, because the syntax suggests that it does _not_, in fact, `put` anything. That is[^running]:
```zig
const std = @import("std");
const print = std.debug.print;
pub fn main() !void {
// You can ignore this - it's setting up an allocator, which is used for reserving and freeing memory
// for Zig's objects. Systems Programming, ahoy! :P
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// "Create a Map<int, List<int>>"
var map = std.AutoHashMap(u32, std.ArrayList(u32)).init(allocator);
// This next line is a pattern of many Systems Programming languages - the necessity to
// tell your system when to free up memory allocated to a variable.
//
// In support of Zig - not only does `defer` (which defers execution until the enclosing function returns)
// make this easier to do, but the toolchain also identifies memory leaks for you.
defer map.deinit();
print("Is the key present _before_ getOrPut? {}\n", .{map.contains(1)});
const response = try map.getOrPut(1);
// At this point, there is still nothing "at" the value - that is, if the following line were uncommented,
// it would cause an error
// print("{}\n", .{response.value_ptr});
//
// But there is a key in the map:
print("Is the key present? {}\n", .{map.contains(1)});
// `response` tells us whether anything was found...
if (!response.found_existing) {
// ...so we can populate an (empty) list at that location...
var list = std.ArrayList(u32).init(allocator);
// This doesn't actually seem to work - see comment near the end of the function
defer list.deinit();
try map.put(1, list);
}
// And add a value to it
try response.value_ptr.append(2);
// For reasons I don't understand, `map.get(1).?.append(2)` doesn't work - the returned
// pointer-to-ArrayList is immutable (`const`), thus preventing appending
// Prove that it worked:
print("The first value of the list is {}\n", .{map.get(1).?.items[0]});
// I don't know why this is necessary, since I already called `defer list.deinit()` above -
// but, without this, I get a memory leak reported
map.get(1).?.deinit();
}
```
Note in particular line the comment after `const response = try map.getOrPut(1)`. There isn't any value "put" into the map!
Many thanks to the folks at [ziggit](https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547) for helping me figure out that I was thinking too high-level about this - adding a key to a HashMap is _not_ a "free" operation, it requires hashing the key (and any associated deduplication of collisions), and reservation of space for the target value[^memory]. In particular, they helped me realize that the return of a `value_ptr` from `getOrPut` means that an optimization is possible in the code I wrote above - I can replace `try map.put(1, list)` (which would require hashing `1` _again_ to determine the target location) with `response.value_ptr.* = list`, "short-circuiting" that calculation for a performance boost.
Awesome! But - [engineers are from Missouri](https://history.howstuffworks.com/american-history/missouri-show-me-state.htm). I deeply appreciate the guidance, but I firmly believe that you don't really internalize a lesson (and shouldn't necessarily trust it) unless you've had it proven - ideally, until you've proven it to yourself. So, here we go!
```zig
const std = @import("std");
const print = std.debug.print;
// Yes, these are low - see discussion below
const TIMES_TO_RUN_A_SINGLE_TEST = 5;
const NUMBER_OF_TESTS_TO_RUN = 5;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Stolen from https://zig.guide/standard-library/random-numbers/
var prng = std.Random.DefaultPrng.init(blk: {
var seed: u64 = undefined;
try std.posix.getrandom(std.mem.asBytes(&seed));
break :blk seed;
});
const rand = prng.random();
// I tried `= try allocator.alloc(u25, NUMBER_OF_TESTS_TO_RUN);`, and got `expected type '[5]u256', found '[]u256'`
// :shrug:
var pointer_based_times: [NUMBER_OF_TESTS_TO_RUN]u256 = undefined;
for (0..NUMBER_OF_TESTS_TO_RUN - 1) |i| {
pointer_based_times[i] = try timePointerBasedMethod(rand, allocator);
}
var non_pointer_based_times: [NUMBER_OF_TESTS_TO_RUN]u256 = undefined;
for (0..NUMBER_OF_TESTS_TO_RUN - 1) |i| {
non_pointer_based_times[i] = try timeNonPointerBasedMethod(rand, allocator);
}
print("Summary of pointer-based times: {}\n", .{summarizeTimes(pointer_based_times)});
print("Summary of non-pointer-based times: {}\n", .{summarizeTimes(non_pointer_based_times)});
}
fn timePointerBasedMethod(rnd: std.Random, allocator: std.mem.Allocator) !u256 {
var map = std.AutoHashMap(u32, std.ArrayList(u32)).init(allocator);
defer map.deinit();
const start_timestamp = std.time.nanoTimestamp();
for (0..TIMES_TO_RUN_A_SINGLE_TEST - 1) |_| {
const key = rnd.int(u32);
const value = rnd.int(u32);
const response = try map.getOrPut(key);
if (!response.found_existing) {
response.value_ptr.* = std.ArrayList(u32).init(allocator);
}
try response.value_ptr.append(value);
}
const time_elapsed = std.time.nanoTimestamp() - start_timestamp;
var it = map.valueIterator();
while (it.next()) |value| {
value.deinit();
}
return try convertTou256(time_elapsed);
}
fn timeNonPointerBasedMethod(rnd: std.Random, allocator: std.mem.Allocator) !u256 {
var map = std.AutoHashMap(u32, std.ArrayList(u32)).init(allocator);
defer map.deinit();
const start_timestamp = std.time.nanoTimestamp();
for (0..TIMES_TO_RUN_A_SINGLE_TEST - 1) |_| {
const key = rnd.int(u32);
const value = rnd.int(u32);
const response = try map.getOrPut(key);
if (!response.found_existing) {
try map.put(key, std.ArrayList(u32).init(allocator));
}
try response.value_ptr.append(value);
}
const time_elapsed = std.time.nanoTimestamp() - start_timestamp;
var it = map.valueIterator();
while (it.next()) |value| {
value.deinit();
}
return try convertTou256(time_elapsed);
}
const ArithmeticError = error{NegativeTime};
fn convertTou256(n: i128) ArithmeticError!u256 {
if (n < 0) {
// This _should_ never happen!
return ArithmeticError.NegativeTime;
} else {
return @intCast(n);
}
}
// Plenty of other stats we could consider, like p90/p99, median, etc.
const Summary = struct {
max: u256,
mean: u256,
};
fn summarizeTimes(times: [NUMBER_OF_TESTS_TO_RUN]u256) Summary {
var total_so_far: u256 = 0;
var max_so_far: u256 = 0;
// For some reason (probably to do with integer overflow :shrug:), I get the occasional time that is _wildly_ large -
// like, 10^60 years big. I already spent an hour futzing with integer types trying to avoid this, to no avail - so
// I'm just filtering out the nonsense instead to get usable data.
var count_of_legal_times: usize = 0;
for (times) |time| {
if (time > 100000000) {
continue;
}
total_so_far += time;
if (time > max_so_far) {
max_so_far = time;
}
count_of_legal_times += 1;
}
return Summary{ .max = max_so_far, .mean = @divFloor(total_so_far, count_of_legal_times) };
}
```
...aaaaand, unfortunately I don't have any convincing data to show. Sequential runs of this program gave inconsistent
results, with neither approach being clearly faster, but (anecdotally) _non_-pointer-based approach seeming to be faster
more often than not:
```
Summary of pointer-based times: test.Summary{ .max = 217000, .mean = 158000 }
Summary of non-pointer-based times: test.Summary{ .max = 138000, .mean = 138000 }
---
Summary of pointer-based times: test.Summary{ .max = 219000, .mean = 158500 }
Summary of non-pointer-based times: test.Summary{ .max = 139000, .mean = 138500 }
---
Summary of pointer-based times: test.Summary{ .max = 216000, .mean = 158000 }
Summary of non-pointer-based times: test.Summary{ .max = 140000, .mean = 139750 }
---
Summary of pointer-based times: test.Summary{ .max = 218000, .mean = 159000 }
Summary of non-pointer-based times: test.Summary{ .max = 278000, .mean = 175750 }
```
You'll note that the variables `TIMES_TO_RUN_A_SINGLE_TEST` and `NUMBER_OF_TESTS_TO_RUN` are super low, so these data are hardly statistically sound. I started out with `1000` and `50` - but experienced a Segmentation Fault (`aborting due to recursive panic`, with a code pointer into `lib/std/array_list.zig`), that I was unable to debug. Even values as low as `10` and `5` caused this issue. I hope I can figure this out to run a more scientific experiment.
Still, even with these low counts - it's surprising for the pointer-based approach to be pretty consistently _slower_. I suspect I'm doing something wrong in my test cases, since the explanation I was given seems intuitively sensible - "_finding the value-location_" twice is always going to be slower than finding it once. I wonder if it's possible that some other part of the test-setup (say, random number generation) dominates the time-spent, and so I'm not actually getting an accurate comparison of pointer-based vs. non-pointer-based interaction with Map Values. I'll try some further experiments in that direction.
UPDATE 2024-12-31 - those fine folks over at Ziggit have [given some pointers on what I might have missed](https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547). I'm spending the last few hours of 2024 trying to progress further in AoC (I don't know if there _is_ a cutoff, but my self-imposed limit is "within 2024"), but I'll try those in the New Year.
[^advent-of-code]: you can see my solutions [here](https://gitea.scubbo.org/scubbo/advent-of-code-2024) - though, since I'd written zero lines of Zig before these challenges, and I've mostly been focused on achieving solutions quickly rather than optimally or maintainably, please don't judge me on Code Quality! 😆
[^rust]: which I used for [last year's Advent Of Code](https://github.com/scubbo/advent-of-code-2023)
[^comparison]: in brief: although I personally find the experience of writing in Systems Programming Languages to be cumbersome, I can absolutely see their value _when used appropriately_; and the mental workout of having to think about memory and efficiency will, I believe, make me a better programmer even in other languages. Sadly, in my professional life we are using GoLang in situations that it is highly unsuited for - specifically, performance-insensitive use-cases, where "_developing fast, in an easily understandable and changable way_" is much more important than "_executing fast_". Having acknowledged that System Programming has its place, however, I disagree with almost every design decision that Rob Pike and co. have made in GoLang - it's not "_bad because it is a Systems Programming Language_", it's "_a bad language that is a Systems Programming Language_". Thankfully, from what I have seen of Zig so far, I like it a _lot_ more!
[^running]: there are plenty of Zig playgrounds/"fiddles" where you can run this without installing Zig on your own system - [e.g.](https://zigfiddle.dev/?mi8GjtRPN50).
[^memory]: I might well have used the wrong terminology there - in particular, I think it's not _allocating_ memory in the `allocator.alloc` sense.

View File

@ -1,53 +0,0 @@
---
title: "Uses"
extraHeadContent:
- <link rel="stylesheet" type="text/css" href="/css/uses-page.css">
---
This is a ["Uses" page](https://uses.tech/), detailing some of the tools and other things I use.
# Technical
## Daily Drive
### Hardware
* **Laptop:** MacBook Pro 15-inch (2018), MacOS Monterey 12.7.1. A gift from my partner (when she in turn was gifted a laptop by her sister) when I quit my job in 2022. I'd like to experiment with a Linux-based daily driver (especially a [Framework](https://frame.work/), or I've heard good things about Linux on Thinkpads), but you can't beat free!
* **Keyboard:** [Kinesis Advantage 2](https://kinesis-ergo.com/shop/advantage2/). It took me about a month to get back up to my usual typing speed after buying this, but the benefits in terms of (lack of) wrist pain are incalculable. And I don't even use the super-leet foot-pedal to switch configuration layouts!
* **Mouse:** Some kind of Razer something that I was gifted by an old housemate.
* **Screens:** Dell 34" Superwide Curved Monitor, plus a LG 32" rotated 90 degrees. I use the latter solely for Slack, Music, and Calendar, whereas the former (in central view) rotates between browser, IDE, terminal, and whatever else (using [Rectangle](https://rectangleapp.com/)).
* [AV Access KVM](https://www.amazon.com/gp/product/B0CP4PD3SM). I only acquired this recently. It's...fine. Unfortunately Macs don't seem to support two external monitors on a single USB-C, so I have to have one of my external monitors directly wired to my work laptop - but that's ok, personal work rarely needs more than my main screen anyway.
### Software
* **IDE:** Visual Studio Code. I used to be a devotee of IntelliJ, but VS Code is the norm at work and it's been easier just to switch than to deal with differing configuration standards, and I don't want to have different IDEs for personal and professional work. I will say that it seems to have lower memory consumption so I can still have it open in the background when I play Factorio!
* **Browser:** Firefox. Continuing to use Chrome in 2024 is just plain baffling. I've yet to hear a compelling case for Edge or IE, or any of the smaller browsers.
* **Terminal:** [Kitty](https://sw.kovidgoyal.net/kitty/). I ditched [iTerm 2](https://iterm2.com/) on principle when they started introducing AI[^ai-optin]. So far they have seemed pretty much equivalent tbh.
* Honourable mention to [Warp](https://www.warp.dev/), which I would definitely pick as a terminal if I was starting out _today_ (if they took out the AI nonsense). It provides out-of-the-box a lot of the customization and usability that I've spent years building up in my own dotfiles.
* [Bitwarden](https://vault.bitwarden.com/) for password management. I'd be interested in moving to self-hosted, but I'd want my backups to be _way_ better-tested before doing so - this is a real central point of failure for life!
* [Rectangle](https://rectangleapp.com) is a super-useful tool to rearrange windows on Mac.
* [fzf](https://www.freecodecamp.org/news/fzf-a-command-line-fuzzy-finder-missing-demo-a7de312403ff/) ([direct GitHub link](https://github.com/junegunn/fzf), but that article does a better job of explaining the value) for Fuzzy Find in the terminal.
* [Tailscale](https://tailscale.com/) for VPN. Believe the hype - it's magical.
## Homelab
### Hardware
* Three Raspberries Pi, and a PowerEdge R430 that I got cheap and refurbished thanks to a tip from a friend.
* [iX Systems](https://www.ixsystems.com/) [TrueNAS R-Series](https://www.truenas.com/r-series/), 64GB RAM, 1x1.9TB SSD, 7x6TB HDD, 4xEmpty for expansion. Probably overkill, but I'd rather give myself some room to grow than have to deal with data migrations and repooling regularly!
* [Sysracks 12U 35" Rack](https://www.amazon.com/gp/product/B09KK678CN).
* [Quotom Mini PC](https://qotom.net/) w/ 8GB RAM, 64GB SSD, running [OPNSense](https://opnsense.org/) as Firewall and Router
* [Ubiquiti UniFi AP AC Pro](https://store.ui.com/us/en/products/uap-ac-pro). I set this up about 4 years ago, and I remember it being a real arse to get working with multiple false starts, but since then it's been pretty much flawless. I briefly experimented with an Eero mesh but that dropped out and needed a restart about every couple of weeks.
### Software
* [k3s](https://k3s.io/) is a super-simple way to install a Kubernetes cluster on "_something as small as a Raspberry Pi_". I'm sure it's probably missing some of the bells-and-whistles of the more fully-featured installations, but I've never hit any limitations that mattered to me. You can see the setup in the [pi-tools](https://github.com/scubbo/pi-tools/tree/main/scripts-on-pi)[^out-of-date-naming] repo that I use to configure my homeserver. Configuration and installation is [just these two lines](https://github.com/scubbo/pi-tools/blob/main/scripts-on-pi/controller_setup/1.sh#L67-L70), though there are another 70 or so lines of installing convenience resources (which I should really migrate to a full GitOps location, but eh, who has the time?)
* [Helm](https://helm.sh/) and [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) are invaluable for defining and deploying Kubernetes applications, respectively.
* I have really enjoyed what tinkering I've done with [cdk8s](https://cdk8s.io/) for Infrastructure-As-Code, but haven't used it in earnest yet. I have been able to use some [jsonnet](https://jsonnet.org/) to achieve some [pretty terse application definitions](https://gitea.scubbo.org/scubbo/helm-charts/src/branch/main/app-of-apps/edh-elo.jsonnet), though.
* [Gitea](https://about.gitea.com/) as my [Git forge](https://gitea.scubbo.org); hosting repos and Docker images, and executing workflows with [Gitea Actions](https://docs.gitea.com/usage/actions/overview)
* [Vault](https://www.hashicorp.com/en/products/vault) for Secrets management
More detail to follow! TL;DR - Grafana, OpenProject, Jellyfin, Crossplane, [democratic-csi](https://github.com/democratic-csi/democratic-csi), KeyCloak, HomeAssistant.
[^ai-optin]: Yes, I know it was opt-in. It still indicates decision-making that I don't want to support.
[^out-of-date-naming]: the naming is somewhat out-of-date, since I've added a non-Pi PowerEdge to the cluster - but hey, all engineers know there's nothing to permanent as a temporary name!

View File

@ -55,7 +55,7 @@
{{- .Content -}}
{{- partial "tags.html" . -}}
<div class="mt6 instapaper_ignoref">
{{ if .Site.Params.DisqusShortname }}
{{ if .Site.DisqusShortname }}
{{ template "_internal/disqus.html" . }}
{{ end }}
{{ if .Site.Params.commentoEnable }}

View File

@ -13,7 +13,7 @@
{{ $.Data.Singular | humanize }}: {{ $key }}
</a>
</h2>
<div style="display: flex; flex-direction: row; flex-wrap: wrap;">
<div class="clearfix w-100">
{{ range $value.Pages }}
{{ partial "tag-summary.html" . }}
{{ end }}

View File

@ -1,75 +0,0 @@
{{/* Copy-pasted from ananke's base `index.html`, with an addition to filter-out "weeknotes" that I don't want to show up on the homepage */}}
{{/* See the post made in the same commit for explanation */}}
{{ define "main" }}
<article class="cf ph3 ph5-l pv3 pv4-l f4 tc-l center measure-wide lh-copy {{ $.Param "text_color" | default "mid-gray" }}">
{{ .Content }}
</article>
{{/* Define a section to pull recent posts from. For Hugo 0.20 this will default to the section with the most number of pages. */}}
{{ $mainSections := .Site.Params.mainSections | default (slice "post") }}
{{/* Check to see if the section is defined for ranging through it */}}
{{range ($mainSections)}}
{{/* Derive the section name */}}
{{ $section_name := . }}
{{/* Create a variable with that section to use in multiple places. */}}
{{ $section_original := where $.Site.RegularPages "Section" "in" $section_name }}
{{ $weeknotes := where $section_original "Params.tags" "intersect" (slice "Weeknotes") }}
{{ $section := complement $weeknotes $section_original }}
{{ $section_count := len $section }}
{{ if ge $section_count 1 }}
<div class="pa3 pa4-ns w-100 w-70-ns center">
{{/* Use $section_name to get the section title. Use "with" to only show it if it exists */}}
{{ with $.Site.GetPage "section" $section_name }}
<h1 class="flex-none">
{{ $.Param "recent_copy" | default (i18n "recentTitle" .) }}
</h1>
{{ end }}
{{ $n_posts := $.Param "recent_posts_number" | default 3 }}
<section class="w-100 mw8">
{{/* Range through the first $n_posts items of the section */}}
{{ range (first $n_posts $section) }}
<div class="relative w-100 mb4">
{{ .Render "summary-with-image" }}
</div>
{{ end }}
</section>
{{ if ge $section_count (add $n_posts 1) }}
<section class="w-100">
<h1 class="f3">{{ i18n "more" }}</h1>
{{/* Now, range through the next four after the initial $n_posts items. Nest the requirements, "after" then "first" on the outside */}}
{{ range (first 4 (after $n_posts $section)) }}
<h2 class="f5 fw4 mb4 dib {{ cond (eq $.Site.Language.LanguageDirection "rtl") "ml3" "mr3" }}">
<a href="{{ .RelPermalink }}" class="link black dim">
{{ .Title }}
</a>
</h2>
{{ end }}
{{/* Add a link to the latest weeknotes */}}
{{ if ge (len $weeknotes) 1 }}
<h1 class="f3">Weeknotes</h1>
{{ range (first 4 $weeknotes) }}
<h2 class="f5 fw4 mb4 dib {{ cond (eq $.Site.Language.LanguageDirection "rtl") "ml3" "mr3" }}">
<a href="{{ .RelPermalink }}" class="link black dim">
{{ .Title }}
</a>
</h2>
{{ end }}
{{ end }}
{{/* As above, Use $section_name to get the section title, and URL. Use "with" to only show it if it exists */}}
{{ with $.Site.GetPage "section" $section_name }}
<a href="{{ .RelPermalink }}" class="link db f6 pa2 br3 bg-mid-gray white dim w4 tc">{{ i18n "allTitle" . }}</a>
{{ end }}
</section>
{{ end }}
</div>
{{ end }}
{{ end }}
{{end}}

View File

@ -4,10 +4,6 @@
{{ end }}
{{ end }}
<!-- https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/ -->
<meta name="fediverse:creator" content="@scubbo@fosstodon.org" />
<!-- This is a prerequisite for the fediverse crediting behaviour on the line above - see https://ogp.me/ -->
<meta property="og:image" content="/img/me.png" />
<!-- https://stackoverflow.com/a/70385342/1040915 -->
<script src="/js/custom.js" defer></script>
<link rel="stylesheet" href="/css/custom.css">
@ -15,10 +11,3 @@
{{ if .Params.math }}{{ partial "helpers/katex.html" . }}{{ end }}
<script defer data-domain="blog.scubbo.org" src="https://tracking.scubbo.org/js/plausible.js"></script>
<!--
https://gohugo.io/templates/rss/
-->
{{ with .OutputFormats.Get "rss" -}}
{{ printf `<link rel=%q type=%q href=%q title=%q>` .Rel .MediaType.Type .Permalink site.Title | safeHTML }}
{{ end }}

View File

@ -1,26 +0,0 @@
{{/*
Copied and adapted from Ananke theme's implementation, removing `.Site.Social` as that is deprecated.
*/}}
{{ $title := .Title }}
{{ $url := printf "%s" .Permalink | absLangURL }}
{{ $facebook_href := printf "https://www.facebook.com/sharer.php?u=%s" $url }}
{{ $twitter_href := printf "https://twitter.com/intent/tweet?url=%s&text=%s" $url $title }}
{{ $twitter_href = printf "%s&via=%s" $twitter_href .Site.Params.twitter }}
{{ $linkedin_href := printf "https://www.linkedin.com/shareArticle?mini=true&url=%s&title=%s" $url $title }}
{{ $hrefs := dict "facebook" $facebook_href "twitter" $twitter_href "linkedin" $linkedin_href }}
{{ $services := where (partialCached "func/socials/Get" "socials/Get") "share" true }}
{{ if not ($.Param "disable_share") }}
<div id="sharing" class="mt3 ananke-socials">
{{ range $service := $services }}
{{ $href := index $hrefs .name }}
<a href="{{ $href }}" class="ananke-social-link {{ .name }} no-underline" aria-label="share on {{ .label }}">
{{ with .icon }}
<span class="icon"> {{ . }}</span>
{{ end }}
</a>
{{ end }}
</div>
{{ end }}

View File

@ -2,16 +2,16 @@
I don't want to _fully_ override the "summary.html" partial that exists in the theme, in case it's used anywhere else -
but this different summary allows me to make the summaries of articles in the tags page a bit more compact.
*/}}
<div class="relative w-30 mb4 bg-white nested-copy-line-height" style="margin-right: 5px; flex-grow:0;">
<div class="bg-white mb3 pa4 gray overflow-hidden">
<span class="f6 db">{{ humanize .Section }}</span>
<h1 class="f3 near-black">
<div class="relative w-30 mb4 bg-white nested-copy-line-height" style="float:left; margin-right: 5px; height: 250px;">
<div class="bg-white mb3 pa4 gray overflow-hidden;" style="height:250px;">
<span class="f6 db">{{ humanize .Section }}</span>
<h1 class="f3 near-black">
<a href="{{ .RelPermalink }}" class="link black dim">
{{ .Title }}
{{ .Title }}
</a>
</h1>
<div class="nested-links f5 lh-copy nested-copy-line-height">
</h1>
<div class="nested-links f5 lh-copy nested-copy-line-height restrict-inner-p-to-250" style="height:250px">
{{ .Summary }}
</div>
</div>
</div>
</div>
</div>

View File

@ -72,3 +72,19 @@ figcaption {
border-bottom: 2px solid #009879;
}
.clearfix::after {
content: "";
clear: both;
display: table;
}
/**
Hacky hack hack...but I can't find where the `<p>` inside the Summary is created
Unforunately, setting a static height doesn't work, because if the header of the summary runs to two lines then this pushes
the `<p>` down further.
*/
.restrict-inner-p-to-250 > p {
height: 96px;
overflow: hidden;
}

View File

@ -1,8 +0,0 @@
table {
border-collapse: collapse;
}
th, td {
border: 1px solid black;
padding: 3px;
}

View File

@ -1,5 +0,0 @@
section.measure-wide {
max-width: none;
width: 95%;
text-align: left;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 8e1a6dc21313d3a765f122c2bd76bdc8ad174e67
Subproject commit 57e38aad389fb114d82e98591d64b5abeba758c9

36
np.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
postName=$1
if [ -z $postName ]; then
echo "Usage: np.sh <postName>"
exit 1
fi
if [ "$#" -ne 1 ]; then
echo "Expected 1 arguments but found $# - exiting"
exit 1
fi
pushd blog > /dev/null
hugo new "posts/$postName.md"
outputLocation="content/posts/$postName.md"
# Use our own env variable to encode which editor
# should be used to edit blogposts. Setting $VISUAL
# to `subl` leads to it also being used by (among
# others) zsh's `edit-command-line`, which is
# undesired
if [ -n "$BLOG_EDITOR" ]; then
$BLOG_EDITOR $outputLocation
elif [ -n "$VISUAL" ]; then
$VISUAL $outputLocation
elif [ -n "$EDITOR" ]; then
$EDITOR $outputLocation
else
echo "No default editor set - falling back to Sublime"
# I expect this is only ever gonna be used by me anyway, so
# I might as well set my own preference as the default :P
subl $outputLocation
fi
popd > /dev/null