The Mixer API

How to make the most of your MIxer subclasses

Mixers are your templating orchestration layer. Inside a mixer, you load various types of dict-yielding things, like YAML/ERB files, Helm charts, or other mixers, then manipulate their output if need be, and submit their final output.

This page is about the Kerbi::Mixer which is a class. Find the complete documentation here.

We use the words "Dict" and "Hash" interchangeably

The essentials: mix() and push()

When you subclass a Kerbi::Mixer, you have to call mix and push if you want to do anything useful. The mix method is what the engine invokes at runtime. Inside your mix method, you call push to say "include this dict or these dicts in the final output".

kerbifile.rb
class TrivialMixer < Kerbi::Mixer
  def mix
    push { hello: "Mister Kerbi" }
    push [{ hola: "Señor Kerbi", bonjour: "Monsieur Kerbi" }] 
  end
end

Kerbi::Globals.mixers << TrivialMixer

Some Observations:

mix() is the method for all your mixer's logic.

push(dicts: Hash | Array<Hash>) adds the dict(s) you give it to the mixer's final output.

Attributes: values and release_name

Mixers are instantiated with two important attributes: values and release_name.

values: Hash is an immutable dict containing the values compiled by Kerbi at start time (gathered from values.yaml, extra values files, and inline --set x=y assignments).

release_name: String holds the release_name value, which is the second argument you pass in the CLI in the template command.

Accessing values and release_name is straightforward:

kerbifile.rb
class HelloMixer < Kerbi::Mixer
  def mix
    push { x: values[:x] } 
    push { x: release_name }
  end
end

Kerbi::Globals.mixers << HelloMixer

It is recommended you use the release_name value for the namespace in you Kubernetes resource descriptors, however it is entirely up to you.

The Dict-Loading Methods

This is the meat of Mixers. The following functions let you load different types of files, and get the result back as a normalized, sanitized list of dicts (i.e Array<Hash>).

kerbifile.rb
class MeddlingMixer < Kerbi::Mixer
  def have_fun!
    extracted = file("yaml")
    puts "I'm just an #{extracted.class} of #{extracted[0].class}!"
    puts "Containing: #{just_dicts}"
  end
end

Testing:

$ kerbi console
irb(kerbi):001:0> mixer = MeddlingMixer.new
irb(kerbi):002:0> mixer.have_fun!
=> I'm just an Array of Hash!
=> Containing: [{key: value, more_key: more_value}, {key: value}]

The dicts() method (aka dict() )

The core dict-loading method, called by every other dict loading method (file() etc...). Has two purposes:

  1. Sanitizing its inputs, turning a single Hash, into an Array<Hash>, transforming non-symbol keys into symbols, raising errors if its inputs are not Hash-like, etc...

  2. Performing post processing according to the options it receives, covered below.

Use it anytime you want to push dicts that did not come directly from another dict loading method (file() etc...). Not doing so and pushing dicts directly can lead to errors.

class DictMixer < Kerbi::Mixer
  def mix
    push dict({"weird_key" => "fixed!"})
  end
end

The file() method

Loads one YAML, JSON, or ERB file containing one or many descriptors that can be turned into dicts.

You can omit the file name extensions, e.g file-one.json can be referred to as "file-one". In general, an extension-less name will trigger a search for:

<name>.yaml
<name>.json
<name>.yaml.erb
<name>.json.erb
kerbifile.rb
class FileMixer < Kerbi::Mixer
  def mix
    push file("file-one")
    push file("dir/file-two")
  end
end

Kerbi::Globals.mixers << FileMixer

The dir() method

Loads all YAML, JSON, or ERB files in a given directory. Scans for the following file extensions:

*.yaml
*.json
*.yaml.erb
*.json.erb
kerbifile.rb
class DirMixer < Kerbi::Mixer
  def mix
    push dir("foo-dir")
  end
end

Kerbi::Globals.mixers << DirMixer

The mixer() method

Instantiates the given mixer, runs it, and returns its output as an Array<Hash>.

kerbifile.rb
require_relative 'other_mixer'

module MultiMixing
  class MixerOne < Kerbi::Mixer
    def mix
      push(mixer_says: "MixerOne #{values}")
    end
  end

  class OuterMixer < Kerbi::Mixer
    def mix
      push mixer_says: "OuterMixer #{values}"
      push mixer(MultiMixing::MixerOne)
      push mixer(MultiMixing::MixerTwo, values: values[:x])
    end
  end
end

Kerbi::Globals.mixers << MultiMixing::OuterMixer
Kerbi::Globals.revision = "1.0.0"

Observations:

  • require_relative imports the other mixer in plain Ruby, no magic

  • **mixer(MultiMixing::MixerOne) ** takes a class, not an instance

  • values: values[:x] lets us customize the values the inner mixer gets

The helm_chart() method

Invokes Helm's template command, i.e helm template [NAME] [CHART] and returns the output as a standard Array<Hash>.

Here is an example using JetStack's cert-manager chart

kerbifile.rb
class HelmExample < Kerbi::Mixer
  def mix
    push cert_manager_resources
  end

  def cert_manager_resources
    helm_chart(
      'jetstack/cert-manager',
      release: release_name,
      values: values.dig(:cert_manager)
    )
  end
end

Kerbi::Globals.mixers << HelmExample

Your local Helm installation must be ready to accept this command, meaning:

  1. The repo must be available to Helm (see helm repo add)

  2. Your helm executable must be available (see Global Configuration)

Post Processing

The patched_with() method

As a convenience, you can have dicts patched onto the dicts that you emit. This is a common pattern for things like annotations and labels on Kubernetes resources.

Only affects dicts processed by dict-loading methods, i.e callers of dict(), so file(), dir(), helm_chart(), and mixer() . If you push() a raw Hash or Array<Hash>, it will not get patched. You can also escape patching in dict-loaders with no_patch: true.

kerbifile.rb
class SimplePatch < Kerbi::Mixer
  def mix 
    datas = { x: { y: "z" } } 
    patch = { x: { y2: "y2" } }

    patched_with patch do
      push dict(datas)
      push datas
    end
  end
end

Output:

$ kerbi template demo .
x:
  y: "z"
  y2: "y2"
--
x: 
  y: "z"

Avoid patching your patches!

You can have nested patches, but make sure that the inner patch itself is not patched with the outer patch. To do this, pass no_patch: true to any dict-loading method you use to load the patch contents:

class SimplePatch < Kerbi::Mixer
  def mix
    patched_with(x: {new_y: "new-z"}) do
      patched_with file("inner-patch", no_patch: true) do
        push business: "as_usual"
      end
    end
  end
end

Filtering Resource Dicts

You can filter the outputs any dict loader method seen above by using the only and except options. Each accepts an Array<Hash> where each Hash should follow the schema:

kind: String | nil # compared to <resource>.kind

name: String | nil # compared to <resource>.metadata.name

class FilteringExample < Kerbi::Mixer
  ONLY = [{kind: "PersistentVolume.*"}]
  EXCEPT = [{name: "unwanted"}]

  def mix
    push file('resources', only: ONLY, except: EXCEPT)
  end
end

Kerbi::Globals.mixers << FilteringExample

Important

  • Omitting name or kind is the same as saying "any" for that attribute.

  • You can pass a quasi regex, which will get interpreted as "^#{your_expr}$. For example, "PersistentVolume.*" will do what you expect and also match "PersistentVolumeClaim".

Last updated