In Progress
Unit 1, Lesson 1
In Progress

Bindings and Local Variables

Have you ever wanted to have precise control over the context that a given given chunk of code executes within? In today’s episode, we’ll use the power of Ruby’s Binding objects to list, display, and set local variables so to tightly manage the environment for some user-supplied code.

Video transcript & code

Bindings and Variables

1. Introduction

We're creating a continuous integration system


variables:
  deploy_secret: 12345ABCDE
  app_name: The Griphon Project
post_build:
  - |-
    require "securerandom"
    build_id = SecureRandom.uuid
  - 'deploy_id = deploy(deploy_secret, package_url)'
  - 'post_to_slack("Deploying #{app_name} - SHA: #{commit_sha}, Build ID: #{build_id}, Deploy ID: #{deploy_id}")'
  
  1. build definition in YAML file
  2. user variable definitions
  3. post-build actions as Ruby code steps
  4. ...first one sets a build ID
  5. ...second one kicks off a deploy
  6. ...third one reports to slack
  7. instance_eval each step
  8. try it out... uh oh, error on deploy_id

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    @build_def["post_build"].each do |post_build_action|
      puts "* POST_BUILD: #{post_build_action.inspect}"
      instance_eval post_build_action # ~> NameError: undefined local variable or method `deploy_secret' for #\nDid you mean?  deploy_id
    end
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
  
# ~> NameError
# ~> undefined local variable or method `deploy_secret' for #
# ~> Did you mean?  deploy_id
# ~>
# ~> (eval):1:in `block in commence'
# ~> buildotron_01.rb:11:in `instance_eval'
# ~> buildotron_01.rb:11:in `block in commence'
# ~> buildotron_01.rb:9:in `each'
# ~> buildotron_01.rb:9:in `commence'
# ~> buildotron_01.rb:18:in `
'

2. Providing user-defined variables

  1. Get a binding object
  2. Use instance_variable_set
  3. Use binding.eval instead of instance_eval
  4. try it out... uh oh, error on package_url

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    @build_def["post_build"].each do |post_build_action|
      build_action_binding = binding # ~> NameError: undefined local variable or method `package_url' for #
      @build_def["variables"].each do |name, value|
        build_action_binding.local_variable_set name, value
      end      
      puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.eval post_build_action
    end
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
  
# ~> NameError
# ~> undefined local variable or method `package_url' for #
# ~>
# ~> buildotron_02.rb:10:in `block in commence'
# ~> buildotron_02.rb:15:in `eval'
# ~> buildotron_02.rb:15:in `block in commence'
# ~> buildotron_02.rb:9:in `each'
# ~> buildotron_02.rb:9:in `commence'
# ~> buildotron_02.rb:22:in `
'

3. Providing system-defined variables

  1. Use instance_variable_set some more
  2. try it out... uh oh, error on deploy method

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence    
    @build_def["post_build"].each do |post_build_action|
      build_action_binding = binding # ~> NoMethodError: undefined method `deploy' for #
      build_action_binding.local_variable_set "package_url", "https://example.com/build1245.zip"
      build_action_binding.local_variable_set "commit_sha", "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
      @build_def["variables"].each do |name, value|
        build_action_binding.local_variable_set name, value
      end      
      puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.eval post_build_action
    end
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
  
# ~> NoMethodError
# ~> undefined method `deploy' for #
# ~>
# ~> buildotron_03.rb:10:in `block in commence'
# ~> buildotron_03.rb:17:in `eval'
# ~> buildotron_03.rb:17:in `block in commence'
# ~> buildotron_03.rb:9:in `each'
# ~> buildotron_03.rb:9:in `commence'
# ~> buildotron_03.rb:24:in `
'

4. Give access to helper methods

  1. Define them locally on the class for now
  2. Placeholder deploy method that just logs
  3. ...build action assigns result to a variable, build_id
  4. ...so we return a hardcoded build ID for now
  5. Placeholder post_to_slack
  6. Run it... works because the binding is in scope where those methods are available
  7. Uh oh... missing build_id

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    @build_def["post_build"].each do |post_build_action|
      build_action_binding = binding # ~> NameError: undefined local variable or method `build_id' for #\nDid you mean?  @build_def
      build_action_binding.local_variable_set "package_url", "https://example.com/build1245.zip"
      build_action_binding.local_variable_set "commit_sha", "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
      @build_def["variables"].each do |name, value|
        build_action_binding.local_variable_set name, value
      end
        puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.eval post_build_action
    end
  end
  
  def deploy(deploy_secret, package_url)
    puts "*** DEPLOY: #{package_url}"
    "DEP-42424242"
  end
  
  def post_to_slack(message)
    puts "*** SLACK: #{message.inspect}"
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
# >> *** DEPLOY: https://example.com/build1245.zip
# >> * POST_BUILD: "post_to_slack(\"Deploying \#{app_name} - SHA: \#{commit_sha}, Build ID: \#{build_id}, Deploy ID: \#{deploy_id}\")"
  
# ~> NameError
# ~> undefined local variable or method `build_id' for #
# ~> Did you mean?  @build_def
# ~>
# ~> buildotron_04.rb:10:in `block in commence'
# ~> buildotron_04.rb:17:in `eval'
# ~> buildotron_04.rb:17:in `block in commence'
# ~> buildotron_04.rb:9:in `each'
# ~> buildotron_04.rb:9:in `commence'
# ~> buildotron_04.rb:33:in `
'

5. Preserve variable values set in build actions

  1. We're grabbing the binding inside each iteration
  2. Move the binding definition out of the build action loop
  3. Run it...now we get all the way to the end!
  4. But what if we want some visibility into variables in post-build steps?

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    build_action_binding = binding
    build_action_binding.local_variable_set "package_url", "https://example.com/build1245.zip"
    build_action_binding.local_variable_set "commit_sha", "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
    @build_def["variables"].each do |name, value|
      build_action_binding.local_variable_set name, value
    end
    @build_def["post_build"].each do |post_build_action|
      puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.eval post_build_action
    end
  end
  
  def deploy(deploy_secret, package_url)
    puts "*** DEPLOY: #{package_url}"
    "DEP-42424242"
  end
  
  def post_to_slack(message)
    puts "*** SLACK: #{message.inspect}"
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
# >> *** DEPLOY: https://example.com/build1245.zip
# >> * POST_BUILD: "post_to_slack(\"Deploying \#{app_name} - SHA: \#{commit_sha}, Build ID: \#{build_id}, Deploy ID: \#{deploy_id}\")"
# >> *** SLACK: "Deploying The Griphon Project - SHA: d670460b4b4aece5915caf5c68d12f560a9fe3e4, Build ID: a9235fa4-6674-417b-af06-0c6b5f8b0be7, Deploy ID: DEP-42424242"

6. Log post-build variables

  1. Iterate through build_action_binding.local_variables
  2. For each, capture its value
  3. Log variable name and value
  4. Run it... Now we can see the variable inputs to each step!
  5. ...including the build_action_binding itself??? We don't want that.

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    build_action_binding = binding
    build_action_binding.local_variable_set "package_url", "https://example.com/build1245.zip"
    build_action_binding.local_variable_set "commit_sha", "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
    @build_def["variables"].each do |name, value|
      build_action_binding.local_variable_set name, value
    end
    @build_def["post_build"].each do |post_build_action|
      puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.local_variables.each do |var_name|
        var_value = build_action_binding.local_variable_get(var_name)
        puts "*** VAR: #{var_name}=#{var_value.inspect}"
      end
      build_action_binding.eval post_build_action
    end
  end
  
  def deploy(deploy_secret, package_url)
    puts "*** DEPLOY: #{package_url}"
    "DEP-42424242"
  end
  
  def post_to_slack(message)
    puts "*** SLACK: #{message.inspect}"
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> *** VAR: build_action_binding=#
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
# >> *** VAR: build_id="a6cbaf09-09be-471e-9cdb-6c214d446e1d"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> *** VAR: build_action_binding=#
# >> *** DEPLOY: https://example.com/build1245.zip
# >> * POST_BUILD: "post_to_slack(\"Deploying \#{app_name} - SHA: \#{commit_sha}, Build ID: \#{build_id}, Deploy ID: \#{deploy_id}\")"
# >> *** VAR: deploy_id="DEP-42424242"
# >> *** VAR: build_id="a6cbaf09-09be-471e-9cdb-6c214d446e1d"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> *** VAR: build_action_binding=#
# >> *** SLACK: "Deploying The Griphon Project - SHA: d670460b4b4aece5915caf5c68d12f560a9fe3e4, Build ID: a6cbaf09-09be-471e-9cdb-6c214d446e1d, Deploy ID: DEP-42424242"

7. Start with a clean binding

  1. Make a method just for returning new bindings in local-variable-free environment
  2. Use the new method
  3. Run it... no more extra variables going into post-build steps

require "yaml"
  
class BuildOTron
  def initialize(build_def)
    @build_def = build_def
  end
  
  def commence
    build_action_binding = new_binding
    build_action_binding.local_variable_set "package_url", "https://example.com/build1245.zip"
    build_action_binding.local_variable_set "commit_sha", "d670460b4b4aece5915caf5c68d12f560a9fe3e4"
    @build_def["variables"].each do |name, value|
      build_action_binding.local_variable_set name, value
    end
    @build_def["post_build"].each do |post_build_action|
      puts "* POST_BUILD: #{post_build_action.inspect}"
      build_action_binding.local_variables.each do |var_name|
        var_value = build_action_binding.local_variable_get(var_name)
        puts "*** VAR: #{var_name}=#{var_value.inspect}"
      end
      build_action_binding.eval post_build_action
    end
  end
  
  def deploy(deploy_secret, package_url)
    puts "*** DEPLOY: #{package_url}"
    "DEP-42424242"
  end
  
  def post_to_slack(message)
    puts "*** SLACK: #{message.inspect}"
  end
  
  def new_binding
    binding
  end
end
  
build_def = YAML.load(File.read(File.dirname(__FILE__) + "/build.yaml"))
build = BuildOTron.new(build_def)
build.commence
  
# >> * POST_BUILD: "require \"securerandom\"\nbuild_id = SecureRandom.uuid"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> * POST_BUILD: "deploy_id = deploy(deploy_secret, package_url)"
# >> *** VAR: build_id="717095bb-0587-4514-a85a-e0351f9d5e04"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> *** DEPLOY: https://example.com/build1245.zip
# >> * POST_BUILD: "post_to_slack(\"Deploying \#{app_name} - SHA: \#{commit_sha}, Build ID: \#{build_id}, Deploy ID: \#{deploy_id}\")"
# >> *** VAR: deploy_id="DEP-42424242"
# >> *** VAR: build_id="717095bb-0587-4514-a85a-e0351f9d5e04"
# >> *** VAR: app_name="The Griphon Project"
# >> *** VAR: deploy_secret="12345ABCDE"
# >> *** VAR: commit_sha="d670460b4b4aece5915caf5c68d12f560a9fe3e4"
# >> *** VAR: package_url="https://example.com/build1245.zip"
# >> *** SLACK: "Deploying The Griphon Project - SHA: d670460b4b4aece5915caf5c68d12f560a9fe3e4, Build ID: 717095bb-0587-4514-a85a-e0351f9d5e04, Deploy ID: DEP-42424242"

8. Conclusion

Responses