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.
@@ -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]
@@ -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
@@ -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
- puts "Transferring #{framework} test suite helpers..."
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
- puts "Transferring #{framework} tests..."
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
- vendored_cookbooks_dir = File.join(storage_directory, 'cookbooks')
65
- berksfile = File.expand_path(@suite['chef']['berksfile'], @suite.repo_root)
68
+ host_cookbooks_dir = File.join(@suite.repo_root, 'cookbooks')
69
+ container_cookbooks_dir = File.join(@suite.chef_run_dir, 'cookbooks')
66
70
 
67
- puts 'Resolving Berksfile...'
68
- @suite.exec!(%w[rm -rf] + [vendored_cookbooks_dir])
69
- Berkshelf::Berksfile.from_file(berksfile).vendor(vendored_cookbooks_dir)
71
+ Navo.synchronize do
72
+ Berksfile.load
70
73
 
71
- @suite.copy(from: File.join(storage_directory, 'cookbooks', '.'),
72
- to: File.join(@suite.chef_run_dir, 'cookbooks'))
73
- end
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
- def install_chef_directories
76
- %w[data_bags environments roles].each do |dir|
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
- @suite.exec(%w[rm -rf] + [container_dir])
82
- @suite.copy(from: host_dir, to: container_dir)
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
- @suite.copy(from: secret_file,
90
- to: File.join(@suite.chef_config_dir, secret_file_basename))
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
- puts 'Preparing solo.rb'
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
- puts 'Preparing first-boot.json'
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
@@ -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
- STDOUT.print chunk
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 exists
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, *@config['docker']['shell-command'])
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['run-list'])
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
- state['converged'] = status == 0
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 converge
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
- if @config['docker']['stop-command']
123
- exec(@config['docker']['stop-command'])
124
- container.wait(@config['docker'].fetch('stop-timeout', 10))
125
- else
126
- container.stop
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
- container.remove(force: true)
229
+ true
230
+ ensure
231
+ @container = nil
232
+ state.destroy
130
233
 
131
- state['converged'] = false
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
- state['images'] ||= {}
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
- image_id = state['images'][dockerfile_hash]
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
- STDOUT.print log['stream']
267
+ @logger.info log['stream']
158
268
  end
159
269
  end.tap do |image|
160
- state['images'][dockerfile_hash] = image.id
161
- state.save
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
- # Continue creating the container since it doesn't exist
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.start
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
- File.join(repo_root, '.navo', 'suites', name)
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 ||= SuiteState.new(suite: self).tap(&:load)
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