navo 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/navo.rb +19 -1
- data/lib/navo/berksfile.rb +58 -0
- data/lib/navo/chef_formatter.rb +278 -0
- data/lib/navo/cli.rb +80 -23
- data/lib/navo/configuration.rb +10 -1
- data/lib/navo/logger.rb +121 -0
- data/lib/navo/sandbox.rb +36 -24
- data/lib/navo/state_file.rb +78 -0
- data/lib/navo/suite.rb +181 -36
- data/lib/navo/utils.rb +17 -0
- data/lib/navo/version.rb +1 -1
- metadata +20 -3
- data/lib/navo/suite_state.rb +0 -59
data/lib/navo/configuration.rb
CHANGED
@@ -78,11 +78,20 @@ module Navo
|
|
78
78
|
# Access the configuration as if it were a hash.
|
79
79
|
#
|
80
80
|
# @param key [String, Symbol]
|
81
|
-
# @return [Array, Hash, Number, String]
|
81
|
+
# @return [Array, Hash, Number, String, Symbol]
|
82
82
|
def [](key)
|
83
83
|
@options[key.to_s]
|
84
84
|
end
|
85
85
|
|
86
|
+
# Set the configuration as if it were a hash.
|
87
|
+
#
|
88
|
+
# @param key [String, Symbol]
|
89
|
+
# @param value [Array, Hash, Number, String, Symbol]
|
90
|
+
# @return [Array, Hash, Number, String]
|
91
|
+
def []=(key, value)
|
92
|
+
@options[key.to_s] = value
|
93
|
+
end
|
94
|
+
|
86
95
|
# Access the configuration as if it were a hash.
|
87
96
|
#
|
88
97
|
# @param key [String, Symbol]
|
data/lib/navo/logger.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Navo
|
6
|
+
# Manages the display of output and writing of log files.
|
7
|
+
#
|
8
|
+
# The goal is to make the tool easier to read when running from the command
|
9
|
+
# line, but preserving all useful information in log files as well so in-depth
|
10
|
+
# debugging can be performed.
|
11
|
+
#
|
12
|
+
# Each test suite creates its own {Output} instance to write to so individual
|
13
|
+
# log lines go to their own files, but they all share a common destination
|
14
|
+
# that is synchronized when writing to stdout/stderr for the application as a
|
15
|
+
# whole.
|
16
|
+
class Logger
|
17
|
+
UI_COLORS = {
|
18
|
+
unknown: 35, # purple
|
19
|
+
fatal: 31, # red
|
20
|
+
error: 31, # red
|
21
|
+
warn: 33, # yellow
|
22
|
+
info: nil, # normal
|
23
|
+
debug: 90, # gray
|
24
|
+
}
|
25
|
+
|
26
|
+
class << self
|
27
|
+
attr_reader :logger
|
28
|
+
|
29
|
+
attr_reader :level
|
30
|
+
|
31
|
+
attr_reader :mutex
|
32
|
+
|
33
|
+
def output=(out)
|
34
|
+
@logger = ::Logger.new(out)
|
35
|
+
@mutex = Mutex.new
|
36
|
+
end
|
37
|
+
|
38
|
+
def level=(level)
|
39
|
+
level = level ? ::Logger.const_get(level.upcase) : ::Logger::INFO
|
40
|
+
@level = level
|
41
|
+
@logger.level = level
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(suite: nil)
|
46
|
+
@suite = suite
|
47
|
+
|
48
|
+
if suite
|
49
|
+
log_file = File.open(suite.log_file, File::CREAT | File::WRONLY | File::APPEND)
|
50
|
+
@logger = ::Logger.new(log_file)
|
51
|
+
@logger.level = self.class.level
|
52
|
+
end
|
53
|
+
|
54
|
+
@color_hash = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def console(message, severity: :info, flush: true)
|
58
|
+
# In order to improve the output displayed, we don't want to print a line
|
59
|
+
# for every chunk of log received--only logs which have newlines.
|
60
|
+
# Thus we buffer the output and flush it once we have the full amount.
|
61
|
+
@buffer_severity ||= severity
|
62
|
+
@buffer_level ||= ::Logger.const_get(severity.upcase)
|
63
|
+
@buffer ||= ''
|
64
|
+
@buffer += message
|
65
|
+
|
66
|
+
flush_buffer if flush
|
67
|
+
end
|
68
|
+
|
69
|
+
def log(severity, message, flush: true)
|
70
|
+
level = ::Logger.const_get(severity.upcase)
|
71
|
+
@logger.add(level, message) if @logger
|
72
|
+
console(message, severity: severity, flush: flush)
|
73
|
+
end
|
74
|
+
|
75
|
+
%i[unknown fatal error warn info debug].each do |severity|
|
76
|
+
define_method severity do |msg|
|
77
|
+
log(severity, msg, flush: true)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def flush_buffer
|
82
|
+
if @buffer
|
83
|
+
# This is shared amongst potentially many threads, so serialize access
|
84
|
+
if @buffer_level >= self.class.level
|
85
|
+
self.class.mutex.synchronize do
|
86
|
+
self.class.logger << pretty_message(@buffer_severity, @buffer)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
@buffer = nil
|
91
|
+
@buffer_severity = nil
|
92
|
+
@buffer_level = nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def pretty_message(severity, message)
|
99
|
+
color_code = UI_COLORS[severity]
|
100
|
+
|
101
|
+
prefix = "[#{@suite.name}] " if @suite
|
102
|
+
colored_prefix = "\e[#{color_for_string(@suite.name)}m#{prefix}\e[0m" if prefix
|
103
|
+
message = message.to_s
|
104
|
+
message = "\e[#{color_code}m#{message}\e[0m" if color_code
|
105
|
+
|
106
|
+
message = indent_output(prefix, colored_prefix, "#{colored_prefix}#{message}")
|
107
|
+
message += "\n" unless message.end_with?("\n")
|
108
|
+
message
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns a deterministic color code for the given string.
|
112
|
+
def color_for_string(string)
|
113
|
+
@color_hash[string] ||= (Digest::MD5.hexdigest(string)[0..8].to_i(16) % 6) + 31
|
114
|
+
end
|
115
|
+
|
116
|
+
def indent_output(prefix, colored_prefix, message)
|
117
|
+
return message unless prefix
|
118
|
+
message.gsub(/\n(?!$)/, "\n#{colored_prefix}")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/lib/navo/sandbox.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'berkshelf'
|
2
1
|
require 'fileutils'
|
3
2
|
require 'pathname'
|
4
3
|
|
@@ -9,13 +8,13 @@ module Navo
|
|
9
8
|
# needed to run a test within the suite's container. A temporary directory on
|
10
9
|
# the host is maintained
|
11
10
|
class Sandbox
|
12
|
-
def initialize(suite:)
|
11
|
+
def initialize(suite:, logger:)
|
13
12
|
@suite = suite
|
13
|
+
@logger = logger
|
14
14
|
end
|
15
15
|
|
16
16
|
def update_chef_config
|
17
17
|
install_cookbooks
|
18
|
-
install_chef_directories
|
19
18
|
install_chef_config
|
20
19
|
end
|
21
20
|
|
@@ -23,6 +22,11 @@ module Navo
|
|
23
22
|
test_files_dir = File.join(@suite.repo_root, %w[test integration])
|
24
23
|
suite_dir = File.join(test_files_dir, @suite.name)
|
25
24
|
|
25
|
+
unless File.exist?(suite_dir)
|
26
|
+
@logger.warn "No test files found at #{suite_dir} for #{@suite.name} suite"
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
26
30
|
# serverspec, bats, etc.
|
27
31
|
frameworks = Pathname.new(suite_dir).children
|
28
32
|
.select(&:directory?)
|
@@ -44,12 +48,12 @@ module Navo
|
|
44
48
|
# between host and container (test-kitchen does the same thing).
|
45
49
|
helpers_directory = File.join(test_files_dir, 'helpers', framework)
|
46
50
|
if File.directory?(helpers_directory)
|
47
|
-
|
51
|
+
@logger.info "Transferring #{framework} test suite helpers..."
|
48
52
|
@suite.copy(from: File.join(helpers_directory, '.'),
|
49
53
|
to: container_framework_dir)
|
50
54
|
end
|
51
55
|
|
52
|
-
|
56
|
+
@logger.info "Transferring #{framework} tests..."
|
53
57
|
@suite.copy(from: File.join(host_framework_dir, '.'),
|
54
58
|
to: container_framework_dir)
|
55
59
|
end
|
@@ -61,45 +65,53 @@ module Navo
|
|
61
65
|
def install_cookbooks
|
62
66
|
@suite.exec!(%w[mkdir -p] + [@suite.chef_config_dir, @suite.chef_run_dir])
|
63
67
|
|
64
|
-
|
65
|
-
|
68
|
+
host_cookbooks_dir = File.join(@suite.repo_root, 'cookbooks')
|
69
|
+
container_cookbooks_dir = File.join(@suite.chef_run_dir, 'cookbooks')
|
66
70
|
|
67
|
-
|
68
|
-
|
69
|
-
Berkshelf::Berksfile.from_file(berksfile).vendor(vendored_cookbooks_dir)
|
71
|
+
Navo.synchronize do
|
72
|
+
Berksfile.load
|
70
73
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
+
# Check all files first so we calculate the hashes
|
75
|
+
berksfile_changed = @suite.path_changed?(Berksfile.path)
|
76
|
+
lockfile_changed = @suite.path_changed?(Berksfile.lockfile_path)
|
77
|
+
cookbooks_changed = @suite.path_changed?(host_cookbooks_dir)
|
74
78
|
|
75
|
-
|
76
|
-
|
77
|
-
puts "Preparing #{dir} directory..."
|
78
|
-
host_dir = File.join(@suite.repo_root, dir)
|
79
|
-
container_dir = File.join(@suite.chef_run_dir, dir)
|
79
|
+
if (berksfile_changed || lockfile_changed || cookbooks_changed) ||
|
80
|
+
@suite.path_changed?(Berksfile.vendor_directory)
|
80
81
|
|
81
|
-
|
82
|
-
|
82
|
+
@logger.info 'Vendoring cookbooks...'
|
83
|
+
Berksfile.vendor(logger: @logger)
|
84
|
+
# Recalculate new hash
|
85
|
+
@suite.path_changed?(Berksfile.vendor_directory)
|
86
|
+
else
|
87
|
+
@logger.info 'No cookbooks changed; nothing new to install'
|
88
|
+
end
|
83
89
|
end
|
84
90
|
end
|
85
91
|
|
86
92
|
def install_chef_config
|
87
93
|
secret_file = File.expand_path(@suite['chef']['secret'], @suite.repo_root)
|
88
94
|
secret_file_basename = File.basename(secret_file)
|
89
|
-
@
|
90
|
-
|
95
|
+
@logger.info "Preparing #{secret_file_basename}"
|
96
|
+
@suite.copy_if_changed(from: secret_file,
|
97
|
+
to: File.join(@suite.chef_config_dir, secret_file_basename))
|
91
98
|
|
92
|
-
|
99
|
+
@logger.info 'Preparing solo.rb'
|
93
100
|
@suite.write(file: File.join(@suite.chef_config_dir, 'solo.rb'),
|
94
101
|
content: @suite.chef_solo_config)
|
95
|
-
|
102
|
+
@logger.info 'Preparing first-boot.json'
|
96
103
|
@suite.write(file: File.join(@suite.chef_config_dir, 'first-boot.json'),
|
97
104
|
content: @suite.node_attributes.to_json)
|
105
|
+
|
106
|
+
@logger.debug 'Installing custom formatter'
|
107
|
+
formatter_file = File.expand_path('chef_formatter.rb', File.dirname(__FILE__))
|
108
|
+
@suite.copy(from: formatter_file, to: @suite.chef_config_dir)
|
98
109
|
end
|
99
110
|
|
100
111
|
def storage_directory
|
101
112
|
@storage_directory ||=
|
102
113
|
@suite.storage_directory.tap do |path|
|
114
|
+
@logger.debug("Ensuring storage directory #{path} exists")
|
103
115
|
FileUtils.mkdir_p(path)
|
104
116
|
end
|
105
117
|
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'monitor'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Navo
|
6
|
+
# Stores persisted state.
|
7
|
+
#
|
8
|
+
# This allows information to carry forward between different invocations of
|
9
|
+
# the tool, e.g. remembering a previously-started Docker container.
|
10
|
+
class StateFile
|
11
|
+
def initialize(file:, logger:)
|
12
|
+
@file = file
|
13
|
+
@logger = logger
|
14
|
+
@mutex = Monitor.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Access the state as if it were a hash.
|
18
|
+
#
|
19
|
+
# @param key [String, Symbol]
|
20
|
+
# @return [Array, Hash, Number, String]
|
21
|
+
def [](key)
|
22
|
+
@mutex.synchronize do
|
23
|
+
@hash[key.to_s]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set the state as if it were a hash.
|
28
|
+
#
|
29
|
+
# @param key [String, Symbol]
|
30
|
+
# @param value [Array, Hash, Number, String]
|
31
|
+
def []=(key, value)
|
32
|
+
@mutex.synchronize do
|
33
|
+
@logger.debug "Updating state '#{key}' to #{value.inspect}"
|
34
|
+
@hash[key.to_s] = value
|
35
|
+
save unless @modifying
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def modify(&block)
|
41
|
+
@mutex.synchronize do
|
42
|
+
@modifying = true
|
43
|
+
begin
|
44
|
+
result = block.call(self)
|
45
|
+
save
|
46
|
+
result
|
47
|
+
ensure
|
48
|
+
@modifying = false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Loads persisted state.
|
54
|
+
def load
|
55
|
+
@hash =
|
56
|
+
if File.exist?(@file) && yaml = YAML.load_file(@file)
|
57
|
+
@logger.debug "Loading state from #{@file}"
|
58
|
+
yaml.to_hash
|
59
|
+
else
|
60
|
+
@logger.debug "No state file #{@file} exists; assuming empty state"
|
61
|
+
{} # Handle empty files
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Persists state to disk.
|
66
|
+
def save
|
67
|
+
@logger.debug "Saving state to #{@file}"
|
68
|
+
File.open(@file, 'w') { |f| f.write(@hash.to_yaml) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Destroy persisted state.
|
72
|
+
def destroy
|
73
|
+
@logger.debug "Removing state from #{@file}"
|
74
|
+
@hash = {}
|
75
|
+
FileUtils.rm_f(@file)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/navo/suite.rb
CHANGED
@@ -8,9 +8,15 @@ module Navo
|
|
8
8
|
class Suite
|
9
9
|
attr_reader :name
|
10
10
|
|
11
|
-
def initialize(name:, config:)
|
11
|
+
def initialize(name:, config:, global_state:)
|
12
12
|
@name = name
|
13
13
|
@config = config
|
14
|
+
@logger = Navo::Logger.new(suite: self)
|
15
|
+
@global_state = global_state
|
16
|
+
|
17
|
+
state.modify do |local|
|
18
|
+
local['files'] ||= {}
|
19
|
+
end
|
14
20
|
end
|
15
21
|
|
16
22
|
def repo_root
|
@@ -35,35 +41,89 @@ module Navo
|
|
35
41
|
|
36
42
|
# Copy file/directory from host to container.
|
37
43
|
def copy(from:, to:)
|
44
|
+
@logger.debug("Copying file #{from} on host to file #{to} in container")
|
38
45
|
system("docker cp #{from} #{container.id}:#{to}")
|
39
46
|
end
|
40
47
|
|
48
|
+
def copy_if_changed(from:, to:, replace: false)
|
49
|
+
if File.directory?(from)
|
50
|
+
exec(%w[mkdir -p] + [to])
|
51
|
+
else
|
52
|
+
exec(%w[mkdir -p] + [File.dirname(to)])
|
53
|
+
end
|
54
|
+
|
55
|
+
current_hash = Utils.path_hash(from)
|
56
|
+
state['files'] ||= {}
|
57
|
+
old_hash = state['files'][from.to_s]
|
58
|
+
|
59
|
+
if !old_hash || current_hash != old_hash
|
60
|
+
if old_hash
|
61
|
+
@logger.debug "Previous hash recorded for #{from} (#{old_hash}) " \
|
62
|
+
"does not match current hash (#{current_hash})"
|
63
|
+
else
|
64
|
+
@logger.debug "No previous hash recorded for #{from}"
|
65
|
+
end
|
66
|
+
|
67
|
+
state.modify do |local|
|
68
|
+
local['files'][from.to_s] = current_hash
|
69
|
+
end
|
70
|
+
|
71
|
+
exec(%w[rm -rf] + [to]) if replace
|
72
|
+
copy(from: from, to: to)
|
73
|
+
return true
|
74
|
+
end
|
75
|
+
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
# TODO: Move to a separate class, since this isn't really suite-specific,
|
80
|
+
# but global to the entire repository.
|
81
|
+
def path_changed?(path)
|
82
|
+
current_hash = Utils.path_hash(path)
|
83
|
+
@global_state['files'] ||= {}
|
84
|
+
old_hash = @global_state['files'][path.to_s]
|
85
|
+
|
86
|
+
@logger.debug("Old hash of #{path.to_s}: #{old_hash}")
|
87
|
+
@logger.debug("Current hash of #{path.to_s}: #{current_hash}")
|
88
|
+
|
89
|
+
@global_state.modify do |local|
|
90
|
+
local['files'][path.to_s] = current_hash
|
91
|
+
end
|
92
|
+
|
93
|
+
!old_hash || current_hash != old_hash
|
94
|
+
end
|
95
|
+
|
41
96
|
# Write contents to a file on the container.
|
42
97
|
def write(file:, content:)
|
98
|
+
@logger.debug("Writing content #{content.inspect} to file #{file} in container")
|
43
99
|
container.exec(%w[bash -c] + ["cat > #{file}"], stdin: StringIO.new(content))
|
44
100
|
end
|
45
101
|
|
46
102
|
# Execte a command on the container.
|
47
|
-
def exec(args)
|
103
|
+
def exec(args, severity: :debug)
|
48
104
|
container.exec(args) do |_stream, chunk|
|
49
|
-
|
105
|
+
@logger.log(severity, chunk, flush: chunk.to_s.end_with?("\n"))
|
50
106
|
end
|
51
107
|
end
|
52
108
|
|
53
|
-
# Execute a command on the container, raising an error if it
|
109
|
+
# Execute a command on the container, raising an error if it exits
|
54
110
|
# unsuccessfully.
|
55
|
-
def exec!(args)
|
56
|
-
out, err, status = exec(args)
|
111
|
+
def exec!(args, severity: :debug)
|
112
|
+
out, err, status = exec(args, severity: severity)
|
57
113
|
raise Error::ExecutionError, "STDOUT:#{out}\nSTDERR:#{err}" unless status == 0
|
58
114
|
[out, err, status]
|
59
115
|
end
|
60
116
|
|
61
117
|
def login
|
62
|
-
Kernel.exec('docker', 'exec', '-it', container.id,
|
118
|
+
Kernel.exec('docker', 'exec', '-it', container.id,
|
119
|
+
*@config['docker'].fetch('shell_command', ['/bin/bash']))
|
63
120
|
end
|
64
121
|
|
65
122
|
def chef_solo_config
|
66
123
|
return <<-CONF
|
124
|
+
load '/etc/chef/chef_formatter.rb'
|
125
|
+
formatter :navo
|
126
|
+
|
67
127
|
node_name #{name.inspect}
|
68
128
|
environment #{@config['chef']['environment'].inspect}
|
69
129
|
file_cache_path #{File.join(chef_run_dir, 'cache').inspect}
|
@@ -78,59 +138,100 @@ module Navo
|
|
78
138
|
|
79
139
|
def node_attributes
|
80
140
|
suite_config = @config['suites'][name]
|
141
|
+
|
142
|
+
unless (run_list = Array(suite_config['run_list'])).any?
|
143
|
+
raise Navo::Errors::ConfigurationError,
|
144
|
+
"No `run_list` specified for suite #{name}!"
|
145
|
+
end
|
146
|
+
|
81
147
|
@config['chef']['attributes']
|
82
148
|
.merge(suite_config.fetch('attributes', {}))
|
83
|
-
.merge(run_list: suite_config['
|
149
|
+
.merge(run_list: suite_config['run_list'])
|
84
150
|
end
|
85
151
|
|
86
152
|
def create
|
153
|
+
@logger.info "=====> Creating #{name}"
|
154
|
+
container
|
155
|
+
@logger.info "=====> Created #{name} in container #{container.id}"
|
87
156
|
container
|
88
157
|
end
|
89
158
|
|
90
159
|
def converge
|
91
160
|
create
|
92
161
|
|
162
|
+
@logger.info "=====> Converging #{name}"
|
93
163
|
sandbox.update_chef_config
|
94
164
|
|
95
165
|
_, _, status = exec(%W[
|
96
166
|
/opt/chef/embedded/bin/chef-solo
|
97
167
|
--config=#{File.join(chef_config_dir, 'solo.rb')}
|
98
168
|
--json-attributes=#{File.join(chef_config_dir, 'first-boot.json')}
|
169
|
+
--format=navo
|
99
170
|
--force-formatter
|
100
|
-
])
|
171
|
+
], severity: :info)
|
101
172
|
|
102
|
-
|
103
|
-
state.save
|
104
|
-
state['converged']
|
173
|
+
status == 0
|
105
174
|
end
|
106
175
|
|
107
176
|
def verify
|
108
177
|
create
|
109
178
|
|
179
|
+
@logger.info "=====> Verifying #{name}"
|
110
180
|
sandbox.update_test_config
|
111
181
|
|
112
|
-
_, _, status = exec(['/usr/bin/env'] + busser_env + %W[#{busser_bin} test]
|
182
|
+
_, _, status = exec(['/usr/bin/env'] + busser_env + %W[#{busser_bin} test],
|
183
|
+
severity: :info)
|
113
184
|
status == 0
|
114
185
|
end
|
115
186
|
|
116
187
|
def test
|
117
|
-
return false unless
|
118
|
-
verify
|
188
|
+
return false unless destroy
|
189
|
+
passed = converge && verify
|
190
|
+
|
191
|
+
should_destroy =
|
192
|
+
case @config['destroy']
|
193
|
+
when 'passing'
|
194
|
+
passed
|
195
|
+
when 'always'
|
196
|
+
true
|
197
|
+
when 'never'
|
198
|
+
false
|
199
|
+
end
|
200
|
+
|
201
|
+
should_destroy ? destroy : passed
|
119
202
|
end
|
120
203
|
|
121
204
|
def destroy
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
205
|
+
@logger.info "=====> Destroying #{name}"
|
206
|
+
|
207
|
+
if state['container']
|
208
|
+
begin
|
209
|
+
if @config['docker']['stop_command']
|
210
|
+
@logger.info "Stopping container #{container.id} via command #{@config['docker']['stop_command']}"
|
211
|
+
exec(@config['docker']['stop_command'])
|
212
|
+
container.wait(@config['docker'].fetch('stop_timeout', 10))
|
213
|
+
else
|
214
|
+
@logger.info "Stopping container #{container.id}..."
|
215
|
+
container.stop
|
216
|
+
end
|
217
|
+
rescue Docker::Error::TimeoutError => ex
|
218
|
+
@logger.warn ex.message
|
219
|
+
ensure
|
220
|
+
begin
|
221
|
+
@logger.info("Removing container #{container.id}")
|
222
|
+
container.remove(force: true)
|
223
|
+
rescue Docker::Error::ServerError => ex
|
224
|
+
@logger.warn ex.message
|
225
|
+
end
|
226
|
+
end
|
127
227
|
end
|
128
228
|
|
129
|
-
|
229
|
+
true
|
230
|
+
ensure
|
231
|
+
@container = nil
|
232
|
+
state.destroy
|
130
233
|
|
131
|
-
|
132
|
-
state['container'] = nil
|
133
|
-
state.save
|
234
|
+
@logger.info "=====> Destroyed #{name}"
|
134
235
|
end
|
135
236
|
|
136
237
|
# Returns the {Docker::Image} used by this test suite, building it if
|
@@ -140,25 +241,35 @@ module Navo
|
|
140
241
|
def image
|
141
242
|
@image ||=
|
142
243
|
begin
|
143
|
-
|
244
|
+
@global_state.modify do |global|
|
245
|
+
global['images'] ||= {}
|
246
|
+
end
|
144
247
|
|
145
248
|
# Build directory is wherever the Dockerfile is located
|
146
249
|
dockerfile = File.expand_path(@config['docker']['dockerfile'], repo_root)
|
147
250
|
build_dir = File.dirname(dockerfile)
|
148
251
|
|
149
252
|
dockerfile_hash = Digest::SHA256.new.hexdigest(File.read(dockerfile))
|
150
|
-
|
253
|
+
@logger.debug "Dockerfile hash is #{dockerfile_hash}"
|
254
|
+
image_id = @global_state['images'][dockerfile_hash]
|
151
255
|
|
152
256
|
if image_id && Docker::Image.exist?(image_id)
|
257
|
+
@logger.debug "Previous image #{image_id} matching Dockerfile already exists"
|
258
|
+
@logger.debug "Using image #{image_id} instead of building new image"
|
153
259
|
Docker::Image.get(image_id)
|
154
260
|
else
|
261
|
+
@logger.debug "No image exists for #{dockerfile}"
|
262
|
+
@logger.debug "Building a new image with #{dockerfile} " \
|
263
|
+
"using #{build_dir} as build context directory"
|
264
|
+
|
155
265
|
Docker::Image.build_from_dir(build_dir) do |chunk|
|
156
266
|
if (log = JSON.parse(chunk)) && log.has_key?('stream')
|
157
|
-
|
267
|
+
@logger.info log['stream']
|
158
268
|
end
|
159
269
|
end.tap do |image|
|
160
|
-
|
161
|
-
|
270
|
+
@global_state.modify do |global|
|
271
|
+
global['images'][dockerfile_hash] = image.id
|
272
|
+
end
|
162
273
|
end
|
163
274
|
end
|
164
275
|
end
|
@@ -171,39 +282,68 @@ module Navo
|
|
171
282
|
def container
|
172
283
|
@container ||=
|
173
284
|
begin
|
285
|
+
# Dummy reference so we build the image first (ensuring its log output
|
286
|
+
# appears before the container creation log output)
|
287
|
+
image
|
288
|
+
|
174
289
|
if state['container']
|
175
290
|
begin
|
176
291
|
container = Docker::Container.get(state['container'])
|
292
|
+
@logger.debug "Loaded existing container #{container.id}"
|
177
293
|
rescue Docker::Error::NotFoundError
|
178
|
-
|
294
|
+
@logger.debug "Container #{state['container']} no longer exists"
|
179
295
|
end
|
180
296
|
end
|
181
297
|
|
182
298
|
if !container
|
299
|
+
@logger.info "Building a new container from image #{image.id}"
|
300
|
+
|
183
301
|
container = Docker::Container.create(
|
184
302
|
'Image' => image.id,
|
185
303
|
'OpenStdin' => true,
|
186
304
|
'StdinOnce' => true,
|
187
305
|
'HostConfig' => {
|
188
306
|
'Privileged' => @config['docker']['privileged'],
|
189
|
-
'Binds' => @config['docker']['volumes']
|
307
|
+
'Binds' => @config['docker']['volumes'] + %W[
|
308
|
+
#{Berksfile.vendor_directory}:#{File.join(chef_run_dir, 'cookbooks')}
|
309
|
+
#{File.join(repo_root, 'data_bags')}:#{File.join(chef_run_dir, 'data_bags')}
|
310
|
+
#{File.join(repo_root, 'environments')}:#{File.join(chef_run_dir, 'environments')}
|
311
|
+
#{File.join(repo_root, 'roles')}:#{File.join(chef_run_dir, 'roles')}
|
312
|
+
],
|
190
313
|
},
|
191
314
|
)
|
192
315
|
|
193
316
|
state['container'] = container.id
|
194
|
-
state.save
|
195
317
|
end
|
196
318
|
|
197
|
-
container.
|
319
|
+
unless started?(container.id)
|
320
|
+
@logger.info "Starting container #{container.id}"
|
321
|
+
container.start
|
322
|
+
else
|
323
|
+
@logger.debug "Container #{container.id} already running"
|
324
|
+
end
|
325
|
+
|
326
|
+
container
|
198
327
|
end
|
199
328
|
end
|
200
329
|
|
330
|
+
def started?(container_id)
|
331
|
+
# There does not appear to be a simple "status" API we can use for an
|
332
|
+
# individual container
|
333
|
+
Docker::Container.all(all: true,
|
334
|
+
filters: { id: [container_id],
|
335
|
+
status: ['running'] }.to_json).any?
|
336
|
+
end
|
337
|
+
|
201
338
|
def sandbox
|
202
|
-
@sandbox ||= Sandbox.new(suite: self)
|
339
|
+
@sandbox ||= Sandbox.new(suite: self, logger: @logger)
|
203
340
|
end
|
204
341
|
|
205
342
|
def storage_directory
|
206
|
-
|
343
|
+
@storage_directory ||=
|
344
|
+
File.join(repo_root, '.navo', 'suites', name).tap do |path|
|
345
|
+
FileUtils.mkdir_p(path)
|
346
|
+
end
|
207
347
|
end
|
208
348
|
|
209
349
|
def busser_directory
|
@@ -224,7 +364,12 @@ module Navo
|
|
224
364
|
end
|
225
365
|
|
226
366
|
def state
|
227
|
-
@state ||=
|
367
|
+
@state ||= StateFile.new(file: File.join(storage_directory, 'state.yaml'),
|
368
|
+
logger: @logger).tap(&:load)
|
369
|
+
end
|
370
|
+
|
371
|
+
def log_file
|
372
|
+
@log_file ||= File.join(storage_directory, 'log.log')
|
228
373
|
end
|
229
374
|
end
|
230
375
|
end
|