Skip to content

Instantly share code, notes, and snippets.

@ripienaar
Last active August 29, 2015 14:21
Show Gist options
  • Select an option

  • Save ripienaar/676df8d9850a3ae0a78b to your computer and use it in GitHub Desktop.

Select an option

Save ripienaar/676df8d9850a3ae0a78b to your computer and use it in GitHub Desktop.

Imagine you have some complex logic you want to make pluggable and extendable. You might use hooks like:

def do_stuff
  before_hook if respond_to?(:before_hook)
  
  # do stuff
  
  after_hook if respond_to?(:after_hook)
end

People inheriting from this can provide hooks your code would call.

But lets say you're an appliance and people are not writing code inheriting from you, they just use your appliance. The appliance deploys stuff:

def deploy!(stuff, deployer)
  ["netapp", "nfs", "server", "os", "vm"].each do |thing|
    send("deploy_#{thing}".to_sym, stuff, deployer)
  end
end

def do_netapp(stuff, deployer)
  # bunch of shitty netapp logic
end

# def do_many_more_things_its_so_bad_please_dont_judge....

This is pretty terrible for oh so many reasons. But this code is inaccessible and no-one can really extend it and if you want to extend it with new support you have to do all sorts of horrible things in whats probably a really massive class full of gross things.

Now you wanted to make this more generic you could do this many ways, but users are still going to not be inheriting from this code but they want to inject custom logic

So lets make some rules for every possible type of thing we do. Here we make netapp volumes whenever there are deployments - and not teardowns:

# rule to deploy netapp stuff
Rules::create_rule(:deploy_netapp) do
  # we want to deploy some stuff
  require_state :stuff, Deployment::Stuff
  
  # we have some deployment helper thing 
  require_state :deployer, Deployer
  
  # a set of named conditions we'll use later, they can inspect any aspect of state
  condition(:is_deployment) { state[:deployer].is_deployment? # vs is_teardown? }

  condition(:is_netapp) do
    state[:stuff]["hardware_type"] == "storage" && state[:stuff]["vendor"] == "netapp"
  end
  
  # now lets combine the named conditionals
  execute_when { is_deployment && is_netapp }
  

  # do this stuff pretty early on
  set_priority 1

  # this is effectively vendor specific hardware logic that only makes sense in the case 
  # of a deployment (and not a tear down) of netapp kit and will only ever be run when incoming
  # stuff is netapp related in a build scenario
  execute do
    volume = My::Thing::NetAppVolumeBuilder.new(state[:stuff])
    volume.build_the_things!
  end
end

But pesky users want to notify their slack channel once we're done building stuff and we dont want to be supporting every possible chat system/notifier etc ever cos we'd never finish....so, user makes a rule and drops it in /etc/mything/rules/slack_all_the_things.rb

Rule::create_rule(:slack_all_the_things) do
  # at the end of every deploy
  priority 999

  require_state :stuff, Deployment::Stuff
  require_state :deployer, Deployer

  condition(:is_deployment) { state[:deployer].is_deployment? || state[:deployer].is_teardown? }
  execute_when { is_deployment }

  execute do
    msg = "Deployment of %s: status: %s" % [state[:deployer].name, state[:deployer].status]

    require 'some_slack_gem'
    Some_slask_gem.do_some_magic_to_post(msg)
  end
end

And now for any rule based logic thats either a deployment or a teardown the user will be notified.

This will also call all the rules thats interested in deploying this stuff in whatever mode deployer is in. All logic extracted and arbitrarily extensible.

Adding new types of hardware like switches doesn't require a change to this code - just rules that detect when stuff is a deployment for new types of hardware.

def deploy!(stuff, deployer)
  rules = Thing::Rules.new
  rules.load_rules!
  
  rules.run_with_state(:stuff => stuff, :deployer => deployer).each do |result|
    if result[:error]
      warn("Processing rule %s failed because: %s: %s" % [result[:name], result[:error].class, result[:error].to_s]
    else
      debug("Processing rule %s passed with output:\n%s" % [result[:name], result[:out]]
     end
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment