Tag

blog

22 posts

Goodreads Microblog


I read a lot. I also run a Ghost blog at partiallypeaceful.com. For years, I'd finish a book, rate it on Goodreads, and that was it. Maybe I'd remember to post something on the blog. Usually I wouldn't.

I wanted every book I read to show up on partiallypeaceful.com. Not as a big review post. Just a short microblog entry: cover image, star rating, a few thoughts if I wrote a review, and a link to buy it somewhere that isn't Amazon.

So I built a cron job. It runs once a day, checks my Goodreads "read" shelf, and publishes anything new to Ghost. The whole thing is about 150 lines of Python. Here's how it works.

The pipeline

Every morning at 8 AM, a cron job on my home server fires `goodreads-to-ghost`. It pulls my Goodreads RSS feed (every shelf gets one at `https://www.goodreads.com/review/list_rss/<user-id>?shelf=read`), parses the XML, and walks through each book in reverse chronological order.

For each book, it checks Ghost for an internal tag called `#goodreads-book-{id}`. If the tag is there, skip. If not, this book is new.

For new books, three things happen. One: the cover image gets downloaded from Goodreads and re-uploaded to Ghost's image library via the Admin API. Goodreads puts weird size suffixes in their cover URLs. The parser strips those out to get the full-resolution original. Two: the post HTML gets assembled. Ghost-hosted cover linked to Goodreads, a "Read [title] by [author]" line, unicode star rating, review text in a blockquote if I wrote one, and a Bookshop.org link. Three: the post gets created through Ghost's Admin API with public tags `microblog` and `books`, plus that invisible `#goodreads-book-{id}` tag.

Once the post is live, Ghost fires a `post.published` webhook. A separate process, `ghost-webhook-forwarder`, sits on port 9001 waiting for it. When the webhook hits, it sends a `repository_dispatch` event to GitHub. That kicks off a GitHub Actions workflow that rebuilds the Astro site. The new book post shows up on partiallypeaceful.com within a couple minutes.

Why RSS instead of the Goodreads API

Goodreads stopped issuing API keys in 2020. Their RSS feeds are still maintained though, and they include everything I need: book ID, title, author, cover URLs, rating, review text, publication date. No authentication required.

The parsing was the only challenge. Goodreads has two different RSS item formats. The standard shelf feed uses custom namespace fields (`book_id`, `book_title`, `author_name`). The updates feed buries the data in HTML descriptions with CSS classes like `bookTitle` and `authorName`. I wrote the parser to handle both. The updates feed parser uses Python's `HTMLParser` from the standard library to pull structured fields out of description blobs. Not elegant, but solid.

Idempotency

Cron jobs fail. Network blips, Ghost doing maintenance, whatever. If the job runs twice, duplicate posts are a bad look.

Internal tags solve this. Before creating a post, the job asks Ghost: "any posts with tag `hash-goodreads-book-{id}`?" If yes, skip. The tag is internal so it doesn't show up in the blog's tag cloud. It's purely operational plumbing.

Dry-run mode is the other safety net. `goodreads-to-ghost --dry-run` prints exactly what it would publish without touching Ghost. I test every change this way before letting cron run unattended.

Zero runtime dependencies

I'm stubborn about dependencies on small tools. Every library is a future maintenance headache. This project has none. No `requests`, no `httpx`, no Ghost SDK. All stdlib: `urllib` for HTTP, `xml.etree.ElementTree` for RSS, `hmac` and `hashlib` for Ghost's JWT-based Admin API auth, `html.parser` for the Goodreads updates feed.

The Ghost Admin API auth was the most satisfying piece. Split the API key into key ID and hex secret, build a JWT with HMAC-SHA256, and send it as a bearer token. About 15 lines of code.

The rebuild pipeline

My blog is a static Astro site on Cloudflare Pages. When Ghost gets a new post, the site needs to be rebuilt to pick it up.

The webhook forwarder bridges that gap. It's a tiny HTTP server on my home server behind Tailscale. Ghost sends `post.published` webhooks to it. The forwarder calls GitHub's `repository_dispatch` API, GitHub Actions runs `npm run build`, and Cloudflare gets the new static files.

It's intentionally bare. Parses the JSON, logs the post title, and fires the dispatch. No queue, no retries, no state. If it fails, it fails. The next webhook catches whatever was missed.

Is this overengineered?

Yeah, probably. I could paste my Goodreads reviews into the Ghost admin manually. 30 seconds per book. But I'd forget. I'd skip weeks. And the point of having a personal blog is that it reflects what I'm actually reading, not what I remembered to post about.

The automation removes the friction completely. Finish a book, rate it on Goodreads, write a review if I have thoughts. It appears on the blog the next morning. I don't think about it.

Your 12-year-old laptop doesn't need replacing. Linux needs tuning.

Linux ships with memory management defaults designed for systems with plenty of RAM. Give it a machine with 3.3 GB and a few modern apps competing for that space, and those defaults are too conservative to prevent freezes.

This is my 2013 MacBook Air – Intel Core i5 Haswell at 1.4 GHz, 3.3 GB RAM, new SSD, new battery, running Ubuntu 26.04. It was locking up about once a session. I was close to buying a replacement. Instead I spent an afternoon on three config changes.

The machine now runs without incident. This is what I changed.


The actual problem

When Chrome, Obsidian, and Ollama compete for 3.3 GB of RAM, the system eventually starts using swap. The kernel's OOM (out-of-memory) killer is supposed to clean this up, but it's a last resort. By the time it activates, the system has already been thrashing swap hard enough that the UI is frozen. The machine is freezing because the kernel lets memory pressure get catastrophic before intervening.

There are three levers to change this.


Lever 1: Know what's happening

Before you can fix a problem, you need to be able to see it.

By default, systemd-journald logs to /run/log/journal – a tmpfs partition in RAM that disappears on reboot. Every crash wipes its own evidence. The fix is one command:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

journald defaults to Storage=auto, which uses /var/log/journal when the directory exists. Creating the directory is all it takes. Logs now survive reboots. After any incident, journalctl --boot=-1 shows what happened.


Lever 2: Intervene earlier

systemd-oomd is a userspace OOM manager. It monitors memory pressure and kills processes before the kernel's OOM killer has to. Userspace kill is surgical: one process dies, the machine stays responsive. Kernel kill is blunt and arrives too late.

The problem: oomd's default thresholds are conservative. Two drop-in configs tune it for a memory-constrained machine.

System-wide: /etc/systemd/oomd.conf.d/99-aggressive.conf

[OOM]
SwapUsedLimitPercent=60%
DefaultMemoryPressureLimit=90%
DefaultMemoryPressureDurationSec=20s

oomd ignores memory pressure below the SwapUsedLimitPercent floor and only acts when sustained pressure exceeds DefaultMemoryPressureLimit for the duration specified. Below 60% swap usage, it stays out of the way entirely.

User session: /etc/systemd/system/user@1000.service.d/99-oomd.conf

[Service]
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=80%
ManagedOOMPreference=avoid

ManagedOOMMemoryPressure=kill is the important one – without this line, oomd only observes. With it, oomd kills. The 80% threshold for user apps is lower than the system 90%, so Chrome dies before anything critical does. ManagedOOMPreference=avoid protects system services.

sudo systemctl restart systemd-oomd

In practice: memory pressure spikes, oomd kills a Chrome process or renderer, the machine keeps running.


Lever 3: Give the system more room

The machine had a 3.8 GB swapfile. With 3.3 GB RAM and oomd's 60% threshold, that left ~2.3 GB of swap before oomd activates – not much runway. Expanding to 8 GB raises that to ~4.8 GB.

sudo swapoff /swapfile
sudo dd if=/dev/zero of=/swapfile bs=1G count=8
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Run this shortly after boot or after closing Chrome. swapoff moves swap contents back to RAM – if you're already 2+ GB into swap, this will itself trigger an OOM kill.


Bonus: Keep Chrome from eating everything in the first place

Auto Tab Discard suspends idle Chrome tabs automatically. Configure it to discard after 10–20 minutes of inactivity. On a 3.3 GB machine with 30+ tabs, this cuts RAM usage by 500 MB to 1 GB before memory pressure ever becomes an issue.


What I tried that didn't work: zram

zram compresses swap data in RAM – faster than disk swap, roughly 3:1 compression ratio. It would be the ideal solution for this machine. But the kernel (6.17-generic on Haswell-ULT) doesn't have the zram module compiled in. Getting it requires a custom kernel. That's a project for another day.


Results

Metric Before After
Swap 3.8 GB 8 GB
OOM kills Kernel (machine freezes) Userspace (process dies, machine survives)
Crash logs Gone on reboot Persistent in /var/log/journal
Daily reboots 1–3 0 (so far)

The hardware was never the problem. A 2013 MacBook Air with a new SSD and a new battery runs Linux fine – it just needs the OS configured for the memory it actually has.

Kernel OOM defaults assume you have room to breathe. Tune oomd to intervene early, give the system enough swap runway, and make sure failures leave evidence. That's it.


May 2026 – 2013 MacBook Air (MacBookAir6,2), Ubuntu 26.04, kernel 6.17

Behind the scenes: RSS, deployment, and crawl hygiene

Over the past few days I've been doing behind-the-scenes work on this site – stuff that doesn't change how anything looks but makes it work properly as a thing that exists on the internet.

RSS feeds. The most user-facing change: the site now has RSS feeds. There's a main feed at /rss.xml covering all posts, plus individual feeds for each tag page, so you can subscribe to just hiking or just software projects. I added this via the @astrojs/rss package, which was straightforward once I had the Ghost content API wired up.

CI/CD pipeline. A surprising amount of work went into the automated deployment pipeline. The site builds with Astro, pulls content from a self-hosted Ghost instance on my home server (accessible over Tailscale), and deploys to Cloudflare Pages. Getting GitHub Actions to reach Ghost required adding a Tailscale step to the workflow. I also upgraded Node.js to v22, bumped Wrangler to v4, and cleared out dependency audit warnings. Classic one-thing-reveals-the-next-thing session.

llms.txt. I added an llms.txt file – an emerging standard for giving AI crawlers a structured plain-text overview of a site. Whether it matters is an open question, but it costs nothing to add.

Sitemap and canonical URLs. The sitemap integration was already installed but not surfaced properly. I added the discovery tag to every page's <head> and corrected the site URL in the Astro config from the staging domain to https://www.partiallypeaceful.com. Canonical URLs, Open Graph tags, and llms.txt all now reference the right domain.

Post truncation. Index page posts now truncate at 125 words with a "continue reading" link. It makes the feed feel like a feed rather than a wall of text.

None of this is glamorous, but it's the kind of thing that makes a site feel finished.

An hour, a Claude sub-agent, and a weird world

Rabbit R1 playing the Grateful Dead

My Rabbit R1 now plays Grateful Dead shows on demand. I built it in an hour last night using a Claude sub-agent and SpecKit. It is completely useless. It might also be a small window into where software is heading.

Here's what I mean.

On Tuesday I wrote about open source software changing in front of us. The argument was pretty simple: when a language model can help you spin up a custom version of almost any tool for your specific needs, the traditional reason to open source something gets murkier. Why share your code when everyone can just build their own?

But then the other half of the argument kicked in. Anthropic's Mythos and models like it represent a new kind of attack surface. The vulnerabilities a sophisticated LLM can exploit, at scale, across networked systems, are genuinely different from what we've dealt with before. When the threat gets that big, the case for collective defense gets stronger. You want audited code, shared security research, pooled resources. You want open source – not because it feels ideologically right, but because no single team can see everything a distributed community of thousands can see.

So we end up with two simultaneous pulls on software culture. One toward the bespoke and personal, one toward the communal and fortified.

That tension got me thinking about a question: what's the most hyper-personal piece of software I could actually build for myself? Not something I'd ship. Not something that scales. Something that exists purely because I want it to exist.

Last night I had an hour. I created a Claude sub-agent with knowledge of how to build Rabbit R1 creations – the little app-like programs that run on the Rabbit device. Then I asked it to build me one that would pull the Today in Grateful Dead show from ReListen and stream it directly from the Rabbit. Between the sub-agent and SpecKit, I one-shotted it. One pass, no debugging spiral, no Stack Overflow detours. The Rabbit plays the Dead now.

None of this is practical. I could just open a browser. The whole thing probably took more effort than it saved for the next decade of use.

But that's kind of the point.

The era of software you build for an audience of one is here, and it's strange. The activation energy has dropped so low that a weird bedtime project can become a working thing before you run out of motivation. There's no product-market fit to consider. There's no user research. There's just: I want this, and now I have it.

That story probably plays out millions of times over the next few years. A lot of hyper-local, hyper-personal software that will never see a GitHub repo, never get a README, never get maintained. It just sits there doing its thing for whoever built it.

Meanwhile, serious teams will lean harder into open source for the parts of the stack that need collective attention. Security primitives. Foundational models. Infrastructure that everyone depends on and everyone benefits from hardening together.

Personal software gets more personal. Shared software gets more deliberately shared.

We live in a weird world.

You can install it here: https://mattfinlayson.github.io/shakedown/qr.html

The repo lives here: https://github.com/mattfinlayson/shakedown

New blog stack: what worked, what didn't

I shut down my VPS. After years of paying to run WordPress on a server I managed myself, Partially Peaceful now runs as a static site on Cloudflare Pages. Here's what I built and what I'd do differently.

The stack

Ghost handles writing and publishing. It runs in a Docker container on a private machine – reachable only over Tailscale, my personal VPN mesh. No public Ghost URL exists. I post from my phone the same way I always did; the Ghost mobile interface is solid.

Cloudflare R2 holds all media. My old WordPress posts are mostly photos, so I needed storage independent of any server. R2 is Cloudflare's S3-compatible object storage. A Ghost storage adapter routes uploads there automatically; media serves from media.partiallypeaceful.com. Ghost URLs get rewritten at fetch time so the built site always points to the right domain.

Astro builds the static site. When I publish in Ghost, a webhook fires to a custom handler, which sends a repository_dispatch event to the GitHub repository. A GitHub Actions workflow connects to the Tailscale network, fetches content from Ghost's Content API using a key stored as a GitHub secret, runs the Astro build, and deploys to Cloudflare Pages via Wrangler.

The build covers: paginated post index, individual post pages, tag archives, /archive, a /now page from a Ghost page by slug, and main plus per-tag RSS feeds – all static.

Styling: Tailwind CSS v4, Nord-inspired palette, fluid typography with clamp(), JetBrains Mono, client-side dark/light toggle via localStorage.

The Ghost container never touches the public internet. Media lives in a bucket. The site is static. Hosting costs nothing.

What worked well

Getting off the VPS entirely. Cloudflare's edge is faster than a single VPS region for most of my traffic, and I no longer think about server upkeep.

Astro as the build layer. Static output, clean component model, straightforward integration with Ghost's Content API. Exactly what a photo-heavy microblog needs.

Tailscale for Ghost access. Keeping Ghost completely private while still letting GitHub Actions reach it during builds was the cleanest part of the whole architecture.

What didn't work well

R2 adapter setup. This took too many attempts. Available Ghost adapters for R2 are scattered and variably maintained; configuration options aren't documented in one place. It works well now, but it was the roughest part of the build and Claude was a big help.

The webhook chain. Ghost fires at a custom handler I wrote, which translates it into a repository_dispatch event for GitHub. That middleman is an extra moving piece I'd rather not maintain. Ghost's native GitHub integration should be able to trigger the Action directly – that's worth revisiting.

What's next

The aggregation layer I want eventually is posts from Mastodon, GitHub, and Goodreads all rolling into a single feed here. That's future work. For anyone considering a similar setup, the bones are solid – the pain is in the R2 adapter documentation and the webhook indirection. Both are solvable.

Shortcut for DayOne journal prompts

DayOne Journal Prompt Shortcut

Download the Shortcut

I’ve been keeping up on my DayOne habit fairly well, but there are some days where staring at that blank page just makes every thought drop out of my head. Initially, I had a lot of success with a simple change, just writing about the previous day. I found that with a little time for reflection I had a more nuanced look at what happened, what was important, and what I thought about it. Lately though I’ve avoided writing some days because I’m not sure what to lay down.

I ran across a shorcut from Brian Renshaw that templated his daily journal. I wanted that, but with more variety.

  1. I started by searching DuckDuckGo for Journal Prompts and blindly copying the first 4 pages of results.
  2. Next I used sheety.co to turn that list of promts into a poor mans API.
  3. I built a shortcut, following Brian’s start and reading documentation as I overthought the process.
  4. Finally, I had to figure out how to share the results, thanks to the iOS 13 beta.

Prompt Sources:

Sharing Shortcuts on iOS 13

I’ve been foolhardy and have been running the iOS 13 beta on all my devices since beta 2. I haven’t hit anything past a few weird layout issues until today. While working on a blog post about a shortcut I built I went to share my work … and there was no share icon.

I looked around, long pressing everywhere and trying every button I could find. I even got desperate and looked around in the Apple Settings screens. Nothing.

After lots of duckduckgoing I found the answer. You need to download a shortcut from the gallery, then go to Settings -> Shortcuts and select “Allow Untrusted Shortcuts”. The option will not exist until you download a shortcut.

Allow Untrusted Shortcuts

Once you allow untrusted shortcuts the share sheet will return to the Shortcut builder. 🙄

Overheard

“Well, you’ll need to get change from the ice cream store to buy a newspaper.”

Dad leafs through his wallet and finds a five. He hands it to Wyatt showing him the note written of the face of the bill in sharpie.

“See what is written on the front? To Aspen from the tooth fairy.”

“Dad … How did you get this?”

“Well I don’t know, they gave it to me at the store.”

“Dad, that raises even more questions, how did this make it to circulation?”

What does your perfect morning look like?

My Ink & Volt Planner has me start each week with a reflection. On a good Monday I can find the time to do it in the morning. Today I managed to get there before the end of day, which is still pretty good.

“If you want to be more productive, you need to become master of your minutes.” — Crystal Pain

Plot out your perfect morning and make it happen one day this week.

Can you master this perfect routine? Practice makes perfect.
  • Wake up early, before the kids, ideally 6am.
  • I went to bed before 10.
  • I have a glass of water, then coffee.
  • I meditate for 20 minutes.
  • I can read or write for 30 minutes.
  • I have time to make my oldest’s lunch and empty the dishwasher.
  • One of the kids wakes up and I have time to hang out with them a bit before the other wakes up.

I was shocked to realize how simple and attainable this was, I hadn’t realized I had been looking for a little space and quiet for myself in the mornings.

Comic Rocket

Although OOTS just published strip #1150 it’s still working on the same original plot line that it started with. At this point some of the early sub-plots and side quests are a bit hazy in my mind and I was thinking it would be nice to catch up. I wanted to read it through my RSS reader, starting over from the first comic, with 5 a day. It seemed like a pretty easy script to write, but I googled for a service first.

I found Comic Rocket which for the bill perfectly. It lets you replay an RSS feed in the increment you want. It also works for podcast feeds.

Choose how you spend your time

Desk calendar for 01/01 on the counter of Barista

A few years ago I was introduced to the idea of replacing New Year’s resolutions with a theme for the year. Rather than tying your success for the new year to specific goal, you pick an area to focus on instead. It’s easier to feel accomplishment if you have several, changeable ways to reach your goals.

The first year I chose ‘Make more things’. It was fun and exciting, we made beer and sourdough. A major renovation took over the whole top floor of the house. My wife and I had a bake-off to see if I would finish the bedroom before she finished the new baby. (I won, but it was a moot point as no one was going to sleep much for a bit).

The next year the theme was ‘Finish more things’. The irony was not lost on anyone around me. It turns out, a theme without a plan leaves you adrift. After a few iterations, I’ve found that having some idea of how I want my theme to play out helps me get what I need out of it. Being gentle with myself and letting things change lets me stick with it.

For this year my theme is ‘Choose how I spend my time’. As always, it’s a little loose, but it fits with several ideas that have kept resurfacing this fall and winter. I’ll know I’m succeeding in 2019 if I am choosing to:

  • Short circuit self-doubt and second thoughts by paying attention to myself, spend 20 minutes a day meditating.
  • Look at my phone when I get to work, put my phone down when I walk in the door at night. If the kids are awake my phone doesn’t have to be. I have an Apple Watch, I’ll know if someone needs to get ahold of me.
  • Spend time with friends and extended family. Share lunch, play games, write letters, and make phone calls.
  • Write more. I started this blog as a challenge and an opportunity. I’ve also been enjoying the micro.blog community very much and will continue to contribute there.
  • Run run run. The last two years I’ve run the Foot Traffic Flat half-marathon. This year I’m going to try to run my first marathon.

2018 was long and tough, 2019 will be too, but I’m optimistic.

On manager README’s

… there is no way to write these and not be self-serving. You are writing them presumably to shortcut problems that arise when people misunderstand your behavior or when they act in a way you don’t like or otherwise violate some expectations that you believe are within your rights to set.



Camille Fournier I hate manager README’s

A couple days a week I run commute to work.

Strava supports recording your run without the phone but will not upload the results without the companion app. It drives me nuts.

It’s impossible to call a Lyft, Car2go, or electric scooter from just the watch. Uber still has a watch app but I won’t support their business.

Text messages may choose to go to your watch … they might go to your iPad or Mac instead. There doesn’t seem to be a clear pattern.

The Washington Post app just won’t refresh without your phone. LTE be damned.

Surprisingly, this is a short list of complaints and the Messages issue is the only one that is serious. For me, it’s almost a complete phone replacement.

Running BaasBox 0.9.4 on Ubuntu 14.04

This is a simple guide to getting BaasBox up and running on Ubuntu. I’m running it on a vps on digital ocean. Right now this is purely for a dev environment and you shouldn’t expect to use the setup for a production site.

First we get our dependencies installed:

# Install Java, Supervisor, and unzipnsudo add-apt-repository ppa:webupd8team/java -ynsudo apt-get updatensudo apt-get install oracle-java8-installer unzip supervisor

Next setup an account to run BaasBox

# Add baasbox user and set up directoriesnsudo adduser baasboxnsudo -u baasboxnsudo mkdir -p /opt/baasboxnsudo mkdir -p /var/log/baasbox

We’ll download the latest verison (0.9.4) of BaasBox and symlink it which will be helpful for later upgrades.

# Download Baasboxncd /optnwget --content-disposition http://www.baasbox.com/download/baasbox-stable.zipnunzip baasbox-stable.zipnsudo chown -R baasbox /var/log/baasbox /opt/baasbox-*nln -s baasbox-0.9.4 baasbox

We’re going to make a couple small changes to the default start script. This will allow us to use a config file for our server and application options, it will also allow us to run BaasBox as a daemon in the next step. Replace /opt/baasbox/start with the following script:

Finally create the server config file at /opt/baasbox/baasbox.conf

We’ll use supervisord, which we installed in the first step to run BaasBox as a service. Supervisord will be responsible for automatically running BaasBox if the process dies or if the machine is rebooted.

# Set up supervisornservice supervisor restart

Create a config file in /etc/supervisor/conf.d/baasbox.conf

Now you should be up and running with BaasBox on port 9000. Remember this is only acceptable for development and please replace the default appcode and admin password.

Swift Playgrounds and 3rd Party Dependencies

Recently, I have been teaching myself swift and exploring 3rd party libraries. It seemed like playgrounds would be the natural way to do this, but I have spent hours trying to figure out how to make it work. My breakthrough finally came when I found this stackoverflow answer explaining the requirements. Immediately after that I found the Apple documentation for the same thing. Of course, the explanations outlined what needed to happen to use external frameworks but didn’t explain how to make it work. With an evening of project creation I was finally able to get something in a working state using cocoapods. This post assumes you have used cocoapods and have it installed in order to proceed.

In XCode do the following:

File -> New -> ProjectnSingle View -> NextnName: ProjectWithDependenciesnChoose a location and click create.

n

Now run the project. I DON’T KNOW WHY, but without this step the workspace is not correctly configured.

File -> File -> iOS -> PlaygroundnName: PlaygroundWithDependenciesnThe default location and options are fine, click create

You now have a project, containing a generic single view application. You also have a playground which says “hello world”.

Now close XCode, open your terminal, and cd to the ProjectWithDependencies directory. For this post we’ll be pulling in Alamofire and Argo to our playground. To do this we will create a cocoapods project.

pod init // create a Podfile

Now edit the Podfile to include our dependencies.

# Uncomment this line to define a global platform for your projectn# platform :ios, '6.0'

use_frameworks!

pod ‘Alamofire’npod ‘Argo’

target ‘PlaygroundWithDependencies’ do

end

target ‘PlaygroundWithDependenciesTests’ do

end

Finally (make sure XCode is closed) and run:

n

pod installnopen ProjectWithDependencies.xcworkspace

n

The last command line step is to actually run pod install to generate the workspace file and pull down the dependencies we just specified.

When we open the workspace the natural thought is to open the Playground and import the new libraries, however, if you do this you’ll see that the project does not recognize them. The final prerequisite is to build the new frameworks. Click and hold on the scheme for ‘ProjectWithDependencies’ in XCode and select ‘Manage Schemes…’

glorious xcode

From there select the check boxes for the Pods we will want to import. For each one choose the scheme and build with the run button or with CMD+R. You must do this for each Pod you with to import. In this case Pods-Alamofire and Pods-Argon

Finally open the playground file itself you can now import Argo and Alamofire without error.

In case you are struggling to follow my steps the final workspace is on github.

BaasBox and Swift - Part 2

In my first post I stepped through the process of porting the skeleton of the BassBox tutorial from Objective-C to Swift. This post takes that project and implements the actual tutorial in Swift. The completed project can be found on this github branch.

The Model

The model is a very basic class with two required fields, the title and body which are both strings. We initialize it by overridding initWithDictionary. The main Swift-ism here is using optional binding to verify the dictionary contains the key / value pairs we expect.

When I have multiple values I need to verify through optional binding I do like using the shorthand syntax of using a single if let statement, remember through it’s only appropriate if you’re looking for a single outcome, if you want to handle multiple combinations (title is set but body is not, etc) you should look at a switch statement.

SMLoginViewController

The tutorial has you copy a completed LoginViewController from the finished project. It’s a rough implementation which uses segmented buttons to toggle between signup and login in code rather than through a storyboard, but it works so for now, let’s convert it to swift.

My first step is to build a class that matches the Objective-C version, then fill in the details. To do this I first create a create a new class in Xcode by going to File -> New -> File -> iOS Source -> Cocoa Touch Class. Then I name the class, choose the appropriate subclass, and choose Swift as the language.

Next I open the original header file, the header defines our properties and methods and gives us a quick way to build our skeleton class. Briefly this:

Becomes this:

Now we need to fill in our instance menthods and implement our required methods for subclassing the UIViewController. The changes were very straight forward with the exception if implementing initWithNibName. The comparable swift method was easy to write:

override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {n    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil);n}

However, Xcode gave me the following error.

SMLoginViewController.swift:16:1: ‘required’ initializer ‘init(coder:)’ must be provided by subclass of ‘UIViewController’

The fix was easy, Xcode was able to generate it, and I was able to find the explanation of what was going on at stackoverflow

Objective-C automatically inherits initWithCoder that’s why we don’t need to add it to destinationViewController.

Swift requires adding init(coder aDecoder: NSCoder!) if you have init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) on your destinationViewController.

Authentication

Moving back to the tutorial, the next step is to perform the the first request to BaasBox. This is done in the viewWillAppear method of the MasterViewController. Once again the Swift implementation was just different syntax with one exception. The call to BaasBox for a collection of SMPosts is done with a completion and it took me a little time to work out the correct syntax.

With the help of fuckingclosuresyntax this becomes:

Also don’t forget inside a closure you must use self where appropriate. This same pattern is repeated for authenticateUser in SMLoginViewController and createNewPost / savePost in SMMasterViewController.

This takes us through the whole tutorial for the DearDiary getting started app.

BaasBox and Swift - Part 1

BaasBox is a tool that allows you to quickly build a backend for an application. The getting started guide for using BaasBox is a little dated and written entirely in Objective-C. The tutorial is fine, but since I’m working in Swift day to day the application I was looking to build would also be in Swift.

My first thought was to just google around and find newer tutorials. I found a single post that seemed relevant, Swifing with BaasBox - Building a quiz app with BaasBox Rather than lose all hope I decided that porting the tutorial app to Swift might be a good exercise in BaasBox and might give me an idea if I liked the tool.

I cloned the starter project and created a swift branch started hacking away.

Next was the brute force method of including the BaasBox SDK:

  1. Downloaded the SDK and dragged the BaasBox-iOS-SDK” folder onto the root of the project.
  2. Created a bridging header to allow swift to import Objective-C source. Go to File -> New -> File -> iOS -> Header File. The naming is very specific, it should be [ProjectName]-Bridging-Header.h’.

In Swift files you can now import BaasBox

The latest SDK and the skeleton project were on different versions so I needed to change some of the tests to get the project to compile.

From there I stepped through and converted each class from Objective-C to Swift. They were straight forward and I was expecting more difficulty. This gets us to the beginning of the tutorial, in the next post we’ll actually implement the tutorial in Swift.

Key listing support in Consul client

For integration between ansible and consul I’ve been using a third party python client called consulate. It is decent, however both it and consul are new and it doesn’t support the full consul HTTP API yet.

Currently I’m trying to model our topology in consul’s key value store but lists of values are not intuitive. Consul seems to only store strings, so without doing string parsing / casting I am unable to get complicated values out.

For example, I’d like to store something like this:

/roles/zookeeper/zones = [‘us-west-2a’, ‘us-west-2b’, ‘us-west-2c’]

and when I do a query against consul I could get back a list to work with. Since I can’t I had been doing terrible things like this:

def get_keys(self):
        output = {}
        for k, v in self.session.kv.items().iteritems():
            if not v is None and type(v) is str and v.startswith("[") and v.endswith("]"):
                output[k] = v.replace('[', '').replace(']', '').split(', ')
            else:
                output[k] = v
        return output

Reading the Consul API docs closer though I found that they have a weird implementation to support this. Specifically:

It is possible to also only list keys without their values by using the “?keys” query parameter along with a GET request. This will return a list of the keys under the given prefix. The optional “?separator=” can be used to list only up to a given separator.

For example, listing “/web/” with a “/” seperator may return:

[ “/web/bar”, “/web/foo”, “/web/subdir/” ] Using the key listing method may be suitable when you do not need the values or flags, or want to implement a key-space explorer.

So I dug into the (simple) consulate find method and added support for ?keys to it. The pull request logged with consulate and we can use my consulate fork for the time being.

Dynamic inventory and variables in Ansible

I’ve been building out automation for deploying micro services in ec2. We’re using consul for service registration, discovery, health checks, and configuration. Since consul provides an available key value store for configuration we’ve been trying to define the topology that way. Ansible has some very good documentation and it is one of the things I like most about the project.

Documentation for building your own dynamic inventory is fairly complete, but I was having trouble with including variables in that inventory. The dynamic inventory documentation shows examples of of host level variables in it’s json output:

eg:

n

{    n"databases": {        n"hosts"   : [ "host1.example.com", "host2.example.com" ],        n"vars"    : {            n"a"   : true        n}    n}n}

n

However, in the standard inventory documentation there is a differnt type of variable, specifically, group variables.

n

[atlanta]nhost1nhost2

[atlanta:vars]nntp_server=ntp.atlanta.example.comnproxy=proxy.atlanta.example.comnnSo my assumption was I could use the group variable syntax in the dynamic inventory output to achive the same thing, the power here was that different consul instances could contain different values allowing me to build a fairly dynamic infrastructure. Combining the documentation from the static inventory with the dynamic output gave me something that looked like this:

n

{    n"databases": {        n"hosts"   : [ "host1.example.com", "host2.example.com" ],        n"vars"    : {            n"a"   : true        n}    n},n"databases:vars": {n"postgres": {n"version": "9.3"n} n}n}

n

Unfortunately ansible-playbook wanted nothing to do with this. ‘databases:vars’ was being cast to a list and treated as a group which was stomping the variables I was trying to pass around. I spent a while thinking about the problem and decided that inventory wasn’t actually where I needed these variables passed in. Instead it would be fine to use consul as a facts source and use that to drive role behavior. I started out trying to augment the magic variable ‘ansible_facts’ by modifying the [setup module] (https://github.com/ansible/ansible/blob/devel/library/system/setup) but ultimately I didn’t want to maintain my own core module. Instead I was able to find two good blog posts about writing a fact gathering module. The first is a little old but comes from one of the best ansible blogs I’ve found. Ansible: variables, variables, and more variables explains the basic approach well but the example code seemed to no longer work. Docker: Containers for the Masses — The docker_facts module is a much more recent post and had a working example to go along with it. It turned out the reason the first example wasn’t working was due to how the module was exiting. The first example was just printing the output, now the correct approach is to use the module.exit_json method.

module.exit_json(changed=changed, ansible_facts=ansible_facts)

n

I’ve posted the module on my ansible fork and may sent a pull request over.

One Way Sync Jira to Things

Little ruby script to take the results of a jql query (advanced search) in jira and create todo item’s for Cultured Code’s Things App. I use it at the beginning of a sprint to keep track of my own work and progress. If I get less lazy I could always sync back in the other direction.

https://gist.github.com/7902002.js?file=config.yaml https://gist.github.com/7902002.js?file=jira_to_things.rb

← All posts