Class: Navo::Suite
- Inherits:
-
Object
- Object
- Navo::Suite
- Defined in:
- lib/navo/suite.rb
Overview
A test suite.
Instance Attribute Summary collapse
-
#name ⇒ Object
readonly
Returns the value of attribute name.
Instance Method Summary collapse
- #[](key) ⇒ Object
- #busser_bin ⇒ Object
- #busser_directory ⇒ Object
- #busser_env ⇒ Object
- #chef_config_dir ⇒ Object
- #chef_run_dir ⇒ Object
- #chef_solo_config ⇒ Object
-
#container ⇒ Docker::Container
Returns the Docker::Container used by this test suite, starting it if necessary.
- #converge ⇒ Object
-
#copy(from:, to:) ⇒ Object
Copy file/directory from host to container.
- #copy_if_changed(from:, to:, replace: false) ⇒ Object
- #create ⇒ Object
- #destroy ⇒ Object
-
#exec(args, severity: :debug) ⇒ Object
Execte a command on the container.
-
#exec!(args, severity: :debug) ⇒ Object
Execute a command on the container, raising an error if it exits unsuccessfully.
- #fetch(key, *args) ⇒ Object
-
#image ⇒ Docker::Image
Returns the Docker::Image used by this test suite, building it if necessary.
-
#initialize(name:, config:, global_state:) ⇒ Suite
constructor
A new instance of Suite.
- #log_file ⇒ Object
- #login ⇒ Object
- #node_attributes ⇒ Object
-
#path_changed?(path) ⇒ Boolean
TODO: Move to a separate class, since this isn’t really suite-specific, but global to the entire repository.
- #repo_root ⇒ Object
- #sandbox ⇒ Object
- #started?(container_id) ⇒ Boolean
- #state ⇒ Object
- #storage_directory ⇒ Object
- #test ⇒ Object
- #verify ⇒ Object
-
#write(file:, content:) ⇒ Object
Write contents to a file on the container.
Constructor Details
#initialize(name:, config:, global_state:) ⇒ Suite
Returns a new instance of Suite.
11 12 13 14 15 16 17 18 19 20 |
# 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 |
Instance Attribute Details
#name ⇒ Object (readonly)
Returns the value of attribute name.
9 10 11 |
# File 'lib/navo/suite.rb', line 9 def name @name end |
Instance Method Details
#[](key) ⇒ Object
26 27 28 |
# File 'lib/navo/suite.rb', line 26 def [](key) @config[key.to_s] end |
#busser_bin ⇒ Object
353 354 355 |
# File 'lib/navo/suite.rb', line 353 def busser_bin File.join(busser_directory, %w[gems bin busser]) end |
#busser_directory ⇒ Object
349 350 351 |
# File 'lib/navo/suite.rb', line 349 def busser_directory '/tmp/busser' end |
#busser_env ⇒ Object
357 358 359 360 361 362 363 364 |
# 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 ⇒ Object
34 35 36 |
# File 'lib/navo/suite.rb', line 34 def chef_config_dir '/etc/chef' end |
#chef_run_dir ⇒ Object
38 39 40 |
# File 'lib/navo/suite.rb', line 38 def chef_run_dir '/var/chef' end |
#chef_solo_config ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/navo/suite.rb', line 122 def chef_solo_config return " load '/etc/chef/chef_formatter.rb'\n formatter :navo\n\n node_name \#{name.inspect}\n environment \#{@config['chef']['environment'].inspect}\n file_cache_path \#{File.join(chef_run_dir, 'cache').inspect}\n file_backup_path \#{File.join(chef_run_dir, 'backup').inspect}\n cookbook_path \#{File.join(chef_run_dir, 'cookbooks').inspect}\n data_bag_path \#{File.join(chef_run_dir, 'data_bags').inspect}\n environment_path \#{File.join(chef_run_dir, 'environments').inspect}\n role_path \#{File.join(chef_run_dir, 'roles').inspect}\n encrypted_data_bag_secret \#{File.join(chef_config_dir, 'encrypted_data_bag_secret').inspect}\n CONF\nend\n" |
#container ⇒ Docker::Container
Returns the Docker::Container used by this test suite, starting it if necessary.
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# 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 ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# 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:) ⇒ Object
Copy file/directory from host to container.
43 44 45 46 |
# 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) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# 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 ⇒ Object
152 153 154 155 156 157 |
# 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 ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# 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. ensure begin @logger.info("Removing container #{container.id}") container.remove(force: true) rescue Docker::Error::ServerError => ex @logger.warn ex. end end end true ensure @container = nil state.destroy @logger.info "=====> Destroyed #{name}" end |
#exec(args, severity: :debug) ⇒ Object
Execte a command on the container.
103 104 105 106 107 |
# 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) ⇒ Object
Execute a command on the container, raising an error if it exits unsuccessfully.
111 112 113 114 115 |
# 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) ⇒ Object
30 31 32 |
# File 'lib/navo/suite.rb', line 30 def fetch(key, *args) @config.fetch(key.to_s, *args) end |
#image ⇒ Docker::Image
Returns the Docker::Image used by this test suite, building it if necessary.
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# 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.(@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 ⇒ Object
371 372 373 |
# File 'lib/navo/suite.rb', line 371 def log_file @log_file ||= File.join(storage_directory, 'log.log') end |
#login ⇒ Object
117 118 119 120 |
# 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 ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 |
# 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) ⇒ Boolean
TODO: Move to a separate class, since this isn’t really suite-specific, but global to the entire repository.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# 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 ⇒ Object
22 23 24 |
# File 'lib/navo/suite.rb', line 22 def repo_root @config.repo_root end |
#sandbox ⇒ Object
338 339 340 |
# File 'lib/navo/suite.rb', line 338 def sandbox @sandbox ||= Sandbox.new(suite: self, logger: @logger) end |
#started?(container_id) ⇒ Boolean
330 331 332 333 334 335 336 |
# 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 ⇒ Object
366 367 368 369 |
# 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 ⇒ Object
342 343 344 345 346 347 |
# 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 ⇒ Object
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# 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 ⇒ Object
176 177 178 179 180 181 182 183 184 185 |
# 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:) ⇒ Object
Write contents to a file on the container.
97 98 99 100 |
# 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 |