🎉 Announcing new lower pricing — up to 40% lower costs for Cloud Servers and Cloud SQL! Read more →

Rubynetes: reflecting with OpenAPI

Story So Far

In the last part of this blog series we used OpenAPI to create a typed Kubernetes manifest that will raise exceptions if you use incorrect attributes. This cuts down on the amount of testing required, but we now have to remember the names of all those types. How can we get the computer to work it out for itself?

A little bit of Ruby metaprogramming should do the trick.

A Small Factory

require 'active_support/core_ext/string/inflections'

def make(target, attribute, attrs = {})
  create_object = lambda { |class_name, attrs|
  new_class = target.class.openapi_types[attribute.to_sym]
    case new_class
    when nil
      raise ArgumentError, attribute
    when /^Array<(?<class_name>\S+)>$/
      [create_object.call(Regexp.last_match[:class_name], attrs)]
    when /^Hash</
      create_object.call(new_class, attrs)

make replaces the creation call for each attribute. It creates an object of the correct type, and assigns it to the correct position in the enclosing object.

A Reflective Config

Our create_job_manifest method can be changed to:

def create_job_manifest(release, config, version, name)
  job = Kubernetes::IoK8sApiBatchV1Job.new(
    api_version: 'batch/v1',
    kind: 'Job'
  make(job, :metadata,
       name: name,
       labels: { build: config[:prefix] })
  make(job, :spec,
       ttl_seconds_after_finished: config[:holdTime])
  make(job.spec, :template)
  make(job.spec.template, :spec,
       restart_policy: 'Never')
  spec = job.spec.template.spec
  make(spec, :volumes,
       name: config[:secretName])
  volume = spec.volumes.first
  make(volume, :secret,
       secret_name: config[:secretName])
  make(volume.secret, :items,
       key: '.dockerconfigjson',
       path: '.docker/config.json')
  make(spec, :containers,
       name: name,
       image: config[:image],
       args: [
  container = spec.containers.first
  make(container, :resources,
       requests: {
         memory: config[:requestsMemory],
         cpu: config[:requestsCpu]
       limits: {
         memory: config[:limitsMemory],
         cpu: config[:limitsCpu]
  make(container, :volume_mounts,
       name: config[:secretName],
       mount_path: '/root')

Now we only have to find the type of the main Kubernetes resource. The heavy nesting has gone and the resource is composed from each of its constituent parts, while retaining the type checking we had previously.

What About build_from_hash?

Sharp-eyed readers will have noticed that the OpenAPI client has a build_from_hash method. Couldn’t we have just sent a Ruby Hash to that method instead of doing metaprogramming?

def create_job_manifest(release, config, version, name)
      apiVersion: 'batch/v1',
      kind: 'Job',
      metadata: {
	name: name,
	labels: {
	  build: config[:prefix]

We could, but this only filters the hash. It doesn’t throw an error if any of the attributes are incorrect. So if you misspell an attribute, it won’t get picked up by the type checking.

Hopefully this will get corrected in a future version of the OpenAPI generator.


By using a full programming language we can create methods that allow us to compose and specialise configurations in a DRY manner.

Once you have further manifests from other tasks, patterns will emerge that can be factored out, perhaps enclosed in a Gem and placed in a Gem repository for future use. Ruby was at the forefront of this approach, and the tooling to support it is very good indeed.

Interested in Managed Kubernetes?

Brightbox have been managing web deployments large and small for over a decade. If you’re interested in the benefits of Kubernetes but want us to handle managing and monitoring it for you, drop us a line.

Get started with Brightbox Sign up takes just two minutes...