class Navo::Suite
A test suite.
Attributes
name[R]
Public Class Methods
new(name:, config:, global_state:)
click to toggle source
# File lib/navo/suite.rb, line 11 def initialize(name:, config:, global_state:) @name = name @config = config @logger = Navo::Logger.new(suite: self) @global_state = global_state state.modify do |local| local['files'] ||= {} end end
Public Instance Methods
[](key)
click to toggle source
# File lib/navo/suite.rb, line 26 def [](key) @config[key.to_s] end
busser_bin()
click to toggle source
# File lib/navo/suite.rb, line 353 def busser_bin File.join(busser_directory, %w[gems bin busser]) end
busser_directory()
click to toggle source
# File lib/navo/suite.rb, line 349 def busser_directory '/tmp/busser' end
busser_env()
click to toggle source
# File lib/navo/suite.rb, line 357 def busser_env %W[ BUSSER_ROOT=#{busser_directory} GEM_HOME=#{File.join(busser_directory, 'gems')} GEM_PATH=#{File.join(busser_directory, 'gems')} GEM_CACHE=#{File.join(busser_directory, %w[gems cache])} ] end
chef_config_dir()
click to toggle source
# File lib/navo/suite.rb, line 34 def chef_config_dir '/etc/chef' end
chef_run_dir()
click to toggle source
# File lib/navo/suite.rb, line 38 def chef_run_dir '/var/chef' end
chef_solo_config()
click to toggle source
# File lib/navo/suite.rb, line 122 def chef_solo_config return <<-CONF load '/etc/chef/chef_formatter.rb' formatter :navo node_name #{name.inspect} environment #{@config['chef']['environment'].inspect} file_cache_path #{File.join(chef_run_dir, 'cache').inspect} file_backup_path #{File.join(chef_run_dir, 'backup').inspect} cookbook_path #{File.join(chef_run_dir, 'cookbooks').inspect} data_bag_path #{File.join(chef_run_dir, 'data_bags').inspect} environment_path #{File.join(chef_run_dir, 'environments').inspect} role_path #{File.join(chef_run_dir, 'roles').inspect} encrypted_data_bag_secret #{File.join(chef_config_dir, 'encrypted_data_bag_secret').inspect} CONF end
container()
click to toggle source
Returns the {Docker::Container} used by this test suite, starting it if necessary.
@return [Docker::Container]
# File lib/navo/suite.rb, line 282 def container @container ||= begin # Dummy reference so we build the image first (ensuring its log output # appears before the container creation log output) image if state['container'] begin container = Docker::Container.get(state['container']) @logger.debug "Loaded existing container #{container.id}" rescue Docker::Error::NotFoundError @logger.debug "Container #{state['container']} no longer exists" end end if !container @logger.info "Building a new container from image #{image.id}" container = Docker::Container.create( 'Image' => image.id, 'OpenStdin' => true, 'StdinOnce' => true, 'HostConfig' => { 'Privileged' => @config['docker']['privileged'], 'Binds' => @config['docker']['volumes'] + %W[ #{Berksfile.vendor_directory}:#{File.join(chef_run_dir, 'cookbooks')} #{File.join(repo_root, 'data_bags')}:#{File.join(chef_run_dir, 'data_bags')} #{File.join(repo_root, 'environments')}:#{File.join(chef_run_dir, 'environments')} #{File.join(repo_root, 'roles')}:#{File.join(chef_run_dir, 'roles')} ], }, ) state['container'] = container.id end unless started?(container.id) @logger.info "Starting container #{container.id}" container.start else @logger.debug "Container #{container.id} already running" end container end end
converge()
click to toggle source
# File lib/navo/suite.rb, line 159 def converge create @logger.info "=====> Converging #{name}" sandbox.update_chef_config _, _, status = exec(%W[ /opt/chef/embedded/bin/chef-solo --config=#{File.join(chef_config_dir, 'solo.rb')} --json-attributes=#{File.join(chef_config_dir, 'first-boot.json')} --format=navo --force-formatter ], severity: :info) status == 0 end
copy(from:, to:)
click to toggle source
Copy file/directory from host to container.
# File lib/navo/suite.rb, line 43 def copy(from:, to:) @logger.debug("Copying file #{from} on host to file #{to} in container") system("docker cp #{from} #{container.id}:#{to}") end
copy_if_changed(from:, to:, replace: false)
click to toggle source
# File lib/navo/suite.rb, line 48 def copy_if_changed(from:, to:, replace: false) if File.directory?(from) exec(%w[mkdir -p] + [to]) else exec(%w[mkdir -p] + [File.dirname(to)]) end current_hash = Utils.path_hash(from) state['files'] ||= {} old_hash = state['files'][from.to_s] if !old_hash || current_hash != old_hash if old_hash @logger.debug "Previous hash recorded for #{from} (#{old_hash}) " \ "does not match current hash (#{current_hash})" else @logger.debug "No previous hash recorded for #{from}" end state.modify do |local| local['files'][from.to_s] = current_hash end exec(%w[rm -rf] + [to]) if replace copy(from: from, to: to) return true end false end
create()
click to toggle source
# File lib/navo/suite.rb, line 152 def create @logger.info "=====> Creating #{name}" container @logger.info "=====> Created #{name} in container #{container.id}" container end
destroy()
click to toggle source
# File lib/navo/suite.rb, line 204 def destroy @logger.info "=====> Destroying #{name}" if state['container'] begin if @config['docker']['stop_command'] @logger.info "Stopping container #{container.id} via command #{@config['docker']['stop_command']}" exec(@config['docker']['stop_command']) container.wait(@config['docker'].fetch('stop_timeout', 10)) else @logger.info "Stopping container #{container.id}..." container.stop end rescue Docker::Error::TimeoutError => ex @logger.warn ex.message ensure begin @logger.info("Removing container #{container.id}") container.remove(force: true) rescue Docker::Error::ServerError => ex @logger.warn ex.message end end end true ensure @container = nil state.destroy @logger.info "=====> Destroyed #{name}" end
exec(args, severity: :debug)
click to toggle source
Execte a command on the container.
# File lib/navo/suite.rb, line 103 def exec(args, severity: :debug) container.exec(args) do |_stream, chunk| @logger.log(severity, chunk, flush: chunk.to_s.end_with?("\n")) end end
exec!(args, severity: :debug)
click to toggle source
Execute a command on the container, raising an error if it exits unsuccessfully.
# File lib/navo/suite.rb, line 111 def exec!(args, severity: :debug) out, err, status = exec(args, severity: severity) raise Error::ExecutionError, "STDOUT:#{out}\nSTDERR:#{err}" unless status == 0 [out, err, status] end
fetch(key, *args)
click to toggle source
# File lib/navo/suite.rb, line 30 def fetch(key, *args) @config.fetch(key.to_s, *args) end
image()
click to toggle source
Returns the {Docker::Image} used by this test suite, building it if necessary.
@return [Docker::Image]
# File lib/navo/suite.rb, line 241 def image @image ||= begin @global_state.modify do |global| global['images'] ||= {} end # Build directory is wherever the Dockerfile is located dockerfile = File.expand_path(@config['docker']['dockerfile'], repo_root) build_dir = File.dirname(dockerfile) dockerfile_hash = Digest::SHA256.new.hexdigest(File.read(dockerfile)) @logger.debug "Dockerfile hash is #{dockerfile_hash}" image_id = @global_state['images'][dockerfile_hash] if image_id && Docker::Image.exist?(image_id) @logger.debug "Previous image #{image_id} matching Dockerfile already exists" @logger.debug "Using image #{image_id} instead of building new image" Docker::Image.get(image_id) else @logger.debug "No image exists for #{dockerfile}" @logger.debug "Building a new image with #{dockerfile} " \ "using #{build_dir} as build context directory" Docker::Image.build_from_dir(build_dir) do |chunk| if (log = JSON.parse(chunk)) && log.has_key?('stream') @logger.info log['stream'] end end.tap do |image| @global_state.modify do |global| global['images'][dockerfile_hash] = image.id end end end end end
log_file()
click to toggle source
# File lib/navo/suite.rb, line 371 def log_file @log_file ||= File.join(storage_directory, 'log.log') end
login()
click to toggle source
# File lib/navo/suite.rb, line 117 def login Kernel.exec('docker', 'exec', '-it', container.id, *@config['docker'].fetch('shell_command', ['/bin/bash'])) end
node_attributes()
click to toggle source
# File lib/navo/suite.rb, line 139 def node_attributes suite_config = @config['suites'][name] unless (run_list = Array(suite_config['run_list'])).any? raise Navo::Errors::ConfigurationError, "No `run_list` specified for suite #{name}!" end @config['chef']['attributes'] .merge(suite_config.fetch('attributes', {})) .merge(run_list: suite_config['run_list']) end
path_changed?(path)
click to toggle source
TODO: Move to a separate class, since this isn’t really suite-specific, but global to the entire repository.
# File lib/navo/suite.rb, line 81 def path_changed?(path) current_hash = Utils.path_hash(path) @global_state['files'] ||= {} old_hash = @global_state['files'][path.to_s] @logger.debug("Old hash of #{path.to_s}: #{old_hash}") @logger.debug("Current hash of #{path.to_s}: #{current_hash}") @global_state.modify do |local| local['files'][path.to_s] = current_hash end !old_hash || current_hash != old_hash end
repo_root()
click to toggle source
# File lib/navo/suite.rb, line 22 def repo_root @config.repo_root end
sandbox()
click to toggle source
# File lib/navo/suite.rb, line 338 def sandbox @sandbox ||= Sandbox.new(suite: self, logger: @logger) end
started?(container_id)
click to toggle source
# File lib/navo/suite.rb, line 330 def started?(container_id) # There does not appear to be a simple "status" API we can use for an # individual container Docker::Container.all(all: true, filters: { id: [container_id], status: ['running'] }.to_json).any? end
state()
click to toggle source
# File lib/navo/suite.rb, line 366 def state @state ||= StateFile.new(file: File.join(storage_directory, 'state.yaml'), logger: @logger).tap(&:load) end
storage_directory()
click to toggle source
# File lib/navo/suite.rb, line 342 def storage_directory @storage_directory ||= File.join(repo_root, '.navo', 'suites', name).tap do |path| FileUtils.mkdir_p(path) end end
test()
click to toggle source
# File lib/navo/suite.rb, line 187 def test return false unless destroy passed = converge && verify should_destroy = case @config['destroy'] when 'passing' passed when 'always' true when 'never' false end should_destroy ? destroy : passed end
verify()
click to toggle source
# File lib/navo/suite.rb, line 176 def verify create @logger.info "=====> Verifying #{name}" sandbox.update_test_config _, _, status = exec(['/usr/bin/env'] + busser_env + %W[#{busser_bin} test], severity: :info) status == 0 end
write(file:, content:)
click to toggle source
Write contents to a file on the container.
# File lib/navo/suite.rb, line 97 def write(file:, content:) @logger.debug("Writing content #{content.inspect} to file #{file} in container") container.exec(%w[bash -c] + ["cat > #{file}"], stdin: StringIO.new(content)) end