navo 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|