Saying stuff about stuff.

Downgrading Kubectl with Homebrew

I started seeing the following error when attempting to list pods with kubectl get pods -n production:

No resources found.
Error from server (NotAcceptable): unknown (get pods)

A quick search led me to a GitHub issue which explained that my version of kubectl was incompatible with the server.

The fix is to revert to an older version but I installed kubectl via Homebrew which only maintains a single version. What I didn’t know is that it’s possible to install a Homebrew package from a URL which makes downgrading easy.

First uninstall the newest version:

brew uninstall kubernetes-cli

Find a compatible version from the history of changes to the kubernetes-cli formula and install:

brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/d09d97241b17a5e02a25fc51fc56e2a5de74501c/Formula/kubernetes-cli.rb

Now everything works again. You should probably pin the newly-installed old version so that it won’t get upgraded the next time you run brew upgrade:

brew pin kubernetes-cli

Dynamically setting default_url_options in Capybara

If you’re developing a full-stack Rails app with a link-based hypermedia API then you may find incorrect URLs breaking your system/feature Capybara specs. What’s going on?

When running your tests Capybara lazily boots the Rails app on a random port and, because this host/port are unknown to Rails, links generated in serializers (and emails) will point to the wrong URL – and following them within your test app and Capybara will fail. Just like dynamically setting Rails default_url_options in Heroku review apps you must tell Rails the host/port on which to build these URLs.

To do this with RSpec you can use RSpec.shared_context to update default_url_options before the example runs and reset it after. Add a support file in spec/support/default_url_options.rb with the following:

original_host = Rails.application.routes.default_url_options[:host]
original_port = Rails.application.routes.default_url_options[:port]

RSpec.shared_context 'default_url_options' do
  before do
    Rails.application.routes.default_url_options[:host] = Capybara.current_session.server.host
    Rails.application.routes.default_url_options[:port] = Capybara.current_session.server.port
  end

  after do
    Rails.application.routes.default_url_options[:host] = original_host
    Rails.application.routes.default_url_options[:port] = original_port
  end
end

(For a reason unknown to me I had to use separate before/after blocks instead of an around block.)

Include it in your browser specs in spec/rails_helper.rb:

RSpec.configure do |config|
  # Traditional feature specs.
  config.include_context 'default_url_options', js: true, type: :feature

  # New fangled system tests.
  config.include_context 'default_url_options', type: :system
end

Now all your _links will point to the correct host/port and you can get on with consuming them in your hypermedia link-driven single page app Rails monolith.

Dynamically setting Rails default_url_options in Heroku review apps

I love Heroku review apps but not when URLs in emails point to the parent app – and if you have a hypermedia API its _links can end up totally broken. Why does this happen and how can it be fixed?

My usual approach used to be to configure Rails.application.routes.default_url_options[:host] from a DEFAULT_URL_HOST environment variable, but if the review app inherits this variable then all of its URLs generated outside the controller/view request/response cycle – including URLs in emails and serializers such as ActiveModel::Serializers – incorrectly point to the parent app. In the past I’ve been forced to manually update a review app’s config but that isn’t a solution.

It isn’t possible to dynamically set an environment variable but you can detect a review app at run-time by enabling the HEROKU_APP_NAME environment variable and then use it to generate the correct host. To do this in Rails create an initializer config/initializers/default_url_options.rb with the following contents:

# If a default host is specifically defined then it's used otherwise the app is
# assumed to be a Heroku review app. Note that `Hash#fetch` is used defensively
# so the app will blow up at boot-time if both `DEFAULT_URL_HOST` and
# `HEROKU_APP_NAME` aren't defined.
host = ENV['DEFAULT_URL_HOST'] ||
  "#{ENV.fetch('HEROKU_APP_NAME')}.herokuapp.com"

# Set the correct protocol as SSL isn't configured in development or test.
protocol = Rails.application.config.force_ssl ? 'https' : 'http'

Rails.application.routes.default_url_options.merge!(
  host: host,
  protocol: protocol,
)

In staging and production apps you should set the DEFAULT_URL_HOST using heroku config:set, but in review apps tell Heroku to pick up the HEROKU_APP_NAME variable by adding it to the app’s app.json (and remove references to DEFAULT_URL_HOST):

{
  "env": {
    "HEROKU_APP_NAME": {
      "required": true
    }
  }
}

Note that this also means in development/test/CI you’ll need to define the DEFAULT_URL_HOST environment variable or the app won’t boot – on my development machine I use direnv, dotenv is also popular.

And that’s it, now all URLs generated in emails and serializers will correctly point to the review app’s host.

RSpec --next-failure

I recently discovered RSpec’s --only-failures feature after seeing Increase Your Quality of Life: An RSpec Primer. I thought I was being clever to combine it with --fail-fast – allowing me to methodically step through failures one at a time – only to discover that the RSpec team already has this covered with the --next-failure option and that they’ve both been available since 2015.

To enable these features example status persistence must be configured in spec/spec_helper.rb (remember to add the path to your .gitignore):

RSpec.configure do |config|
  config.example_status_persistence_file_path = "spec/examples.txt"
end

Now rspec --only-failures and rspec --next-failure will work as advertised. They can also be combined with an additional path filter and so will work with an entire directory, a single file, or any other RSpec path filter:

rspec --next-failure
rspec --next-failure spec/system
rspec --next-failure spec/models/foo_spec.rb
rspec --next-failure spec/models/foo_spec.rb[1:2]

These RSpec nuggets have made me much more efficient when encountering a large swathes of failing tests, it makes me wonder of what else I’m unaware.

v now automatically reads from stdin

I previously wrote v to help with a couple of my common wants when starting Vim. Something I often forget is to pass the - when piping stdin to Vim, expecting it to behave in the same way as other commands like less. So I’ve updated v to automatically pass the - argument to Vim if it detects that it’s receiving content from stdin.

Here’s what now happens:

$ echo testy | v
Vim: Reading from stdin...

As ever the source is on GitHub. For the curious here are the tests for this newly added functionality.