Unit 1, Lesson 21
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}")'
- build definition in YAML file
- user variable definitions
- post-build actions as Ruby code steps
- ...first one sets a build ID
- ...second one kicks off a deploy
- ...third one reports to slack
instance_eval
each step- 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
- Get a binding object
- Use
instance_variable_set
- Use
binding.eval
instead ofinstance_eval
- 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
- Use
instance_variable_set
some more - 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
- Define them locally on the class for now
- Placeholder
deploy
method that just logs - ...build action assigns result to a variable,
build_id
- ...so we return a hardcoded build ID for now
- Placeholder
post_to_slack
- Run it... works because the binding is in scope where those methods are available
- 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
- We're grabbing the binding inside each iteration
- Move the binding definition out of the build action loop
- Run it...now we get all the way to the end!
- 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
- Iterate through
build_action_binding.local_variables
- For each, capture its value
- Log variable name and value
- Run it... Now we can see the variable inputs to each step!
- ...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
- Make a method just for returning new bindings in local-variable-free environment
- Use the new method
- 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"
Responses