navo 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3adda44205c06f6444cdc4969561ce867498cc6f
4
+ data.tar.gz: a8261f98b2e072aab9451df37b558c4b2376308a
5
+ SHA512:
6
+ metadata.gz: ec8ecc5aee52a8c00aeaab4ffed1062f613f71a85b808327d1c0dd62e9d0326793a5e1cbe4f49a1628d46038b0b050e8ddc7884b90a1d9c9cc6dae39a4fa02a3
7
+ data.tar.gz: 940a3980f2acae81e2353e7c57d2a79d523bb7fa2d572f6b28a9c28bc8bdc1d96e02339aa55e0e5e0b97fca54387c1ee2bcffd793a2b2e4f5175bad72cc178e4
data/bin/navo ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'navo/cli'
4
+
5
+ Navo::CLI.start(ARGV)
data/lib/navo.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'navo/constants'
2
+ require 'navo/errors'
3
+ require 'navo/configuration'
4
+ require 'navo/sandbox'
5
+ require 'navo/suite'
6
+ require 'navo/suite_state'
7
+ require 'navo/utils'
8
+ require 'navo/version'
data/lib/navo/cli.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'berkshelf'
2
+ require 'navo'
3
+ require 'thor'
4
+
5
+ module Navo
6
+ # Command line application interface.
7
+ class CLI < Thor
8
+ desc 'create', 'create a container for test suite(s) to run within'
9
+ def create(pattern = nil)
10
+ exit suites_for(pattern).map(&:create).all? ? 0 : 1
11
+ end
12
+
13
+ desc 'converge', 'run Chef for test suite(s)'
14
+ def converge(pattern = nil)
15
+ exit suites_for(pattern).map(&:converge).all? ? 0 : 1
16
+ end
17
+
18
+ desc 'verify', 'run test suite(s)'
19
+ def verify(pattern = nil)
20
+ exit suites_for(pattern).map(&:verify).all? ? 0 : 1
21
+ end
22
+
23
+ desc 'test', 'converge and run test suite(s)'
24
+ def test(pattern = nil)
25
+ exit suites_for(pattern).map(&:test).all? ? 0 : 1
26
+ end
27
+
28
+ desc 'destroy', 'clean up test suite(s)'
29
+ def destroy(pattern = nil)
30
+ exit suites_for(pattern).map(&:destroy).all? ? 0 : 1
31
+ end
32
+
33
+ desc 'login', "open a shell inside a suite's container"
34
+ def login(pattern)
35
+ suites = suites_for(pattern)
36
+ if suites.size > 1
37
+ puts 'Pattern matched more than one test suite'
38
+ exit 1
39
+ else
40
+ suites.first.login
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def config
47
+ @config ||= Configuration.load_applicable
48
+ end
49
+
50
+ def suites_for(pattern)
51
+ suite_names = config['suites'].keys
52
+ suite_names.select! { |name| name =~ /#{pattern}/ } if pattern
53
+
54
+ suite_names.map do |suite_name|
55
+ Suite.new(name: suite_name, config: config)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ require 'pathname'
2
+ require 'yaml'
3
+
4
+ module Navo
5
+ # Stores runtime configuration for the application.
6
+ #
7
+ # This is intended to define helper methods for accessing configuration so
8
+ # this logic can be shared amongst the various components of the system.
9
+ class Configuration
10
+ # Name of the configuration file.
11
+ FILE_NAME = '.navo.yaml'
12
+
13
+ class << self
14
+ # Loads appropriate configuration file given the current working
15
+ # directory.
16
+ #
17
+ # @return [Navo::Configuration]
18
+ def load_applicable
19
+ current_directory = File.expand_path(Dir.pwd)
20
+ config_file = applicable_config_file(current_directory)
21
+
22
+ if config_file
23
+ from_file(config_file)
24
+ else
25
+ raise Errors::ConfigurationMissingError,
26
+ "No configuration file '#{FILE_NAME}' was found in the " \
27
+ "current directory or any ancestor directory.\n\n" \
28
+ "See #{REPO_URL}#configuration for instructions on setting up."
29
+ end
30
+ end
31
+
32
+ # Loads a configuration from a file.
33
+ #
34
+ # @return [Navo::Configuration]
35
+ def from_file(config_file)
36
+ options =
37
+ if yaml = YAML.load_file(config_file)
38
+ yaml.to_hash
39
+ else
40
+ {}
41
+ end
42
+
43
+ new(options: options, path: config_file)
44
+ end
45
+
46
+ private
47
+
48
+ # Returns the first valid configuration file found, starting from the
49
+ # current working directory and ascending to ancestor directories.
50
+ #
51
+ # @param directory [String]
52
+ # @return [String, nil]
53
+ def applicable_config_file(directory)
54
+ Pathname.new(directory)
55
+ .enum_for(:ascend)
56
+ .map { |dir| dir + FILE_NAME }
57
+ .find do |config_file|
58
+ config_file if config_file.exist?
59
+ end
60
+ end
61
+ end
62
+
63
+ # Creates a configuration from the given options hash.
64
+ #
65
+ # @param options [Hash]
66
+ def initialize(options:, path:)
67
+ @options = options
68
+ @path = path
69
+ end
70
+
71
+ # Returns the root of the repository to which this configuration applies.
72
+ #
73
+ # @return [String]
74
+ def repo_root
75
+ File.dirname(@path)
76
+ end
77
+
78
+ # Access the configuration as if it were a hash.
79
+ #
80
+ # @param key [String, Symbol]
81
+ # @return [Array, Hash, Number, String]
82
+ def [](key)
83
+ @options[key.to_s]
84
+ end
85
+
86
+ # Access the configuration as if it were a hash.
87
+ #
88
+ # @param key [String, Symbol]
89
+ # @return [Array, Hash, Number, String]
90
+ def fetch(key, *args)
91
+ @options.fetch(key.to_s, *args)
92
+ end
93
+
94
+ # Compares this configuration with another.
95
+ #
96
+ # @param other [HamlLint::Configuration]
97
+ # @return [true,false] whether the given configuration is equivalent
98
+ def ==(other)
99
+ super || @options == other.instance_variable_get('@options')
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,7 @@
1
+ # Global application constants.
2
+ module Navo
3
+ EXECUTABLE_NAME = 'navo'
4
+
5
+ REPO_URL = 'https://github.com/sds/navo'
6
+ BUG_REPORT_URL = "#{REPO_URL}/issues"
7
+ end
@@ -0,0 +1,14 @@
1
+ # Collection of errors that can be thrown by the application.
2
+ module Navo::Errors
3
+ # Base class for all errors reported by this tool.
4
+ class NavoError < StandardError; end
5
+
6
+ # Raised when a command on a container fails.
7
+ class ExecutionError < NavoError; end
8
+
9
+ # Base class for all configuration-related errors.
10
+ class ConfigurationError < NavoError; end
11
+
12
+ # Raised when a configuration file is not present.
13
+ class ConfigurationMissingError < ConfigurationError; end
14
+ end
@@ -0,0 +1,107 @@
1
+ require 'berkshelf'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+
5
+ module Navo
6
+ # Manages the creation and maintenance of the test suite sandbox.
7
+ #
8
+ # The sandbox is just a directory that contains the files and configuration
9
+ # needed to run a test within the suite's container. A temporary directory on
10
+ # the host is maintained
11
+ class Sandbox
12
+ def initialize(suite:)
13
+ @suite = suite
14
+ end
15
+
16
+ def update_chef_config
17
+ install_cookbooks
18
+ install_chef_directories
19
+ install_chef_config
20
+ end
21
+
22
+ def update_test_config
23
+ test_files_dir = File.join(@suite.repo_root, %w[test integration])
24
+ suite_dir = File.join(test_files_dir, @suite.name)
25
+
26
+ # serverspec, bats, etc.
27
+ frameworks = Pathname.new(suite_dir).children
28
+ .select(&:directory?)
29
+ .map(&:basename)
30
+ .map(&:to_s)
31
+
32
+ suites_directory = File.join(@suite.busser_directory, 'suites')
33
+ @suite.exec!(%w[mkdir -p] + [suites_directory])
34
+
35
+ frameworks.each do |framework|
36
+ host_framework_dir = File.join(suite_dir, framework)
37
+ container_framework_dir = File.join(suites_directory, framework)
38
+
39
+ @suite.exec!(%w[rm -rf] + [container_framework_dir])
40
+
41
+ # In order to work with Busser, we need to copy the helper files into
42
+ # the same directory as the suite's spec files. This avoids issues with
43
+ # symlinks not matching up due to differences in directory structure
44
+ # between host and container (test-kitchen does the same thing).
45
+ helpers_directory = File.join(test_files_dir, 'helpers', framework)
46
+ if File.directory?(helpers_directory)
47
+ puts "Transferring #{framework} test suite helpers..."
48
+ @suite.copy(from: File.join(helpers_directory, '.'),
49
+ to: container_framework_dir)
50
+ end
51
+
52
+ puts "Transferring #{framework} tests..."
53
+ @suite.copy(from: File.join(host_framework_dir, '.'),
54
+ to: container_framework_dir)
55
+ end
56
+
57
+ end
58
+
59
+ private
60
+
61
+ def install_cookbooks
62
+ @suite.exec!(%w[mkdir -p] + [@suite.chef_config_dir, @suite.chef_run_dir])
63
+
64
+ vendored_cookbooks_dir = File.join(storage_directory, 'cookbooks')
65
+ berksfile = File.expand_path(@suite['chef']['berksfile'], @suite.repo_root)
66
+
67
+ puts 'Resolving Berksfile...'
68
+ @suite.exec!(%w[rm -rf] + [vendored_cookbooks_dir])
69
+ Berkshelf::Berksfile.from_file(berksfile).vendor(vendored_cookbooks_dir)
70
+
71
+ @suite.copy(from: File.join(storage_directory, 'cookbooks', '.'),
72
+ to: File.join(@suite.chef_run_dir, 'cookbooks'))
73
+ end
74
+
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)
80
+
81
+ @suite.exec(%w[rm -rf] + [container_dir])
82
+ @suite.copy(from: host_dir, to: container_dir)
83
+ end
84
+ end
85
+
86
+ def install_chef_config
87
+ secret_file = File.expand_path(@suite['chef']['secret'], @suite.repo_root)
88
+ secret_file_basename = File.basename(secret_file)
89
+ @suite.copy(from: secret_file,
90
+ to: File.join(@suite.chef_config_dir, secret_file_basename))
91
+
92
+ puts 'Preparing solo.rb'
93
+ @suite.write(file: File.join(@suite.chef_config_dir, 'solo.rb'),
94
+ content: @suite.chef_solo_config)
95
+ puts 'Preparing first-boot.json'
96
+ @suite.write(file: File.join(@suite.chef_config_dir, 'first-boot.json'),
97
+ content: @suite.node_attributes.to_json)
98
+ end
99
+
100
+ def storage_directory
101
+ @storage_directory ||=
102
+ @suite.storage_directory.tap do |path|
103
+ FileUtils.mkdir_p(path)
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/navo/suite.rb ADDED
@@ -0,0 +1,230 @@
1
+ require 'docker'
2
+ require 'digest'
3
+ require 'json'
4
+ require 'shellwords'
5
+
6
+ module Navo
7
+ # A test suite.
8
+ class Suite
9
+ attr_reader :name
10
+
11
+ def initialize(name:, config:)
12
+ @name = name
13
+ @config = config
14
+ end
15
+
16
+ def repo_root
17
+ @config.repo_root
18
+ end
19
+
20
+ def [](key)
21
+ @config[key.to_s]
22
+ end
23
+
24
+ def fetch(key, *args)
25
+ @config.fetch(key.to_s, *args)
26
+ end
27
+
28
+ def chef_config_dir
29
+ '/etc/chef'
30
+ end
31
+
32
+ def chef_run_dir
33
+ '/var/chef'
34
+ end
35
+
36
+ # Copy file/directory from host to container.
37
+ def copy(from:, to:)
38
+ system("docker cp #{from} #{container.id}:#{to}")
39
+ end
40
+
41
+ # Write contents to a file on the container.
42
+ def write(file:, content:)
43
+ container.exec(%w[bash -c] + ["cat > #{file}"], stdin: StringIO.new(content))
44
+ end
45
+
46
+ # Execte a command on the container.
47
+ def exec(args)
48
+ container.exec(args) do |_stream, chunk|
49
+ STDOUT.print chunk
50
+ end
51
+ end
52
+
53
+ # Execute a command on the container, raising an error if it exists
54
+ # unsuccessfully.
55
+ def exec!(args)
56
+ out, err, status = exec(args)
57
+ raise Error::ExecutionError, "STDOUT:#{out}\nSTDERR:#{err}" unless status == 0
58
+ [out, err, status]
59
+ end
60
+
61
+ def login
62
+ Kernel.exec('docker', 'exec', '-it', container.id, *@config['docker']['shell-command'])
63
+ end
64
+
65
+ def chef_solo_config
66
+ return <<-CONF
67
+ node_name #{name.inspect}
68
+ environment #{@config['chef']['environment'].inspect}
69
+ file_cache_path #{File.join(chef_run_dir, 'cache').inspect}
70
+ file_backup_path #{File.join(chef_run_dir, 'backup').inspect}
71
+ cookbook_path #{File.join(chef_run_dir, 'cookbooks').inspect}
72
+ data_bag_path #{File.join(chef_run_dir, 'data_bags').inspect}
73
+ environment_path #{File.join(chef_run_dir, 'environments').inspect}
74
+ role_path #{File.join(chef_run_dir, 'roles').inspect}
75
+ encrypted_data_bag_secret #{File.join(chef_config_dir, 'encrypted_data_bag_secret').inspect}
76
+ CONF
77
+ end
78
+
79
+ def node_attributes
80
+ suite_config = @config['suites'][name]
81
+ @config['chef']['attributes']
82
+ .merge(suite_config.fetch('attributes', {}))
83
+ .merge(run_list: suite_config['run-list'])
84
+ end
85
+
86
+ def create
87
+ container
88
+ end
89
+
90
+ def converge
91
+ create
92
+
93
+ sandbox.update_chef_config
94
+
95
+ _, _, status = exec(%W[
96
+ /opt/chef/embedded/bin/chef-solo
97
+ --config=#{File.join(chef_config_dir, 'solo.rb')}
98
+ --json-attributes=#{File.join(chef_config_dir, 'first-boot.json')}
99
+ --force-formatter
100
+ ])
101
+
102
+ state['converged'] = status == 0
103
+ state.save
104
+ state['converged']
105
+ end
106
+
107
+ def verify
108
+ create
109
+
110
+ sandbox.update_test_config
111
+
112
+ _, _, status = exec(['/usr/bin/env'] + busser_env + %W[#{busser_bin} test])
113
+ status == 0
114
+ end
115
+
116
+ def test
117
+ return false unless converge
118
+ verify
119
+ end
120
+
121
+ 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
127
+ end
128
+
129
+ container.remove(force: true)
130
+
131
+ state['converged'] = false
132
+ state['container'] = nil
133
+ state.save
134
+ end
135
+
136
+ # Returns the {Docker::Image} used by this test suite, building it if
137
+ # necessary.
138
+ #
139
+ # @return [Docker::Image]
140
+ def image
141
+ @image ||=
142
+ begin
143
+ state['images'] ||= {}
144
+
145
+ # Build directory is wherever the Dockerfile is located
146
+ dockerfile = File.expand_path(@config['docker']['dockerfile'], repo_root)
147
+ build_dir = File.dirname(dockerfile)
148
+
149
+ dockerfile_hash = Digest::SHA256.new.hexdigest(File.read(dockerfile))
150
+ image_id = state['images'][dockerfile_hash]
151
+
152
+ if image_id && Docker::Image.exist?(image_id)
153
+ Docker::Image.get(image_id)
154
+ else
155
+ Docker::Image.build_from_dir(build_dir) do |chunk|
156
+ if (log = JSON.parse(chunk)) && log.has_key?('stream')
157
+ STDOUT.print log['stream']
158
+ end
159
+ end.tap do |image|
160
+ state['images'][dockerfile_hash] = image.id
161
+ state.save
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Returns the {Docker::Container} used by this test suite, starting it if
168
+ # necessary.
169
+ #
170
+ # @return [Docker::Container]
171
+ def container
172
+ @container ||=
173
+ begin
174
+ if state['container']
175
+ begin
176
+ container = Docker::Container.get(state['container'])
177
+ rescue Docker::Error::NotFoundError
178
+ # Continue creating the container since it doesn't exist
179
+ end
180
+ end
181
+
182
+ if !container
183
+ container = Docker::Container.create(
184
+ 'Image' => image.id,
185
+ 'OpenStdin' => true,
186
+ 'StdinOnce' => true,
187
+ 'HostConfig' => {
188
+ 'Privileged' => @config['docker']['privileged'],
189
+ 'Binds' => @config['docker']['volumes'],
190
+ },
191
+ )
192
+
193
+ state['container'] = container.id
194
+ state.save
195
+ end
196
+
197
+ container.start
198
+ end
199
+ end
200
+
201
+ def sandbox
202
+ @sandbox ||= Sandbox.new(suite: self)
203
+ end
204
+
205
+ def storage_directory
206
+ File.join(repo_root, '.navo', 'suites', name)
207
+ end
208
+
209
+ def busser_directory
210
+ '/tmp/busser'
211
+ end
212
+
213
+ def busser_bin
214
+ File.join(busser_directory, %w[gems bin busser])
215
+ end
216
+
217
+ def busser_env
218
+ %W[
219
+ BUSSER_ROOT=#{busser_directory}
220
+ GEM_HOME=#{File.join(busser_directory, 'gems')}
221
+ GEM_PATH=#{File.join(busser_directory, 'gems')}
222
+ GEM_CACHE=#{File.join(busser_directory, %w[gems cache])}
223
+ ]
224
+ end
225
+
226
+ def state
227
+ @state ||= SuiteState.new(suite: self).tap(&:load)
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,59 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Navo
5
+ # Stores persisted state about a test suite.
6
+ #
7
+ # This allows information to carry forward between different invocations of
8
+ # the tool, e.g. remembering a previously-started Docker container.
9
+ class SuiteState
10
+ FILE_NAME = 'state.yaml'
11
+
12
+ def initialize(suite:)
13
+ @suite = suite
14
+ end
15
+
16
+ # Access the state as if it were a hash.
17
+ #
18
+ # @param key [String, Symbol]
19
+ # @return [Array, Hash, Number, String]
20
+ def [](key)
21
+ @hash[key.to_s]
22
+ end
23
+
24
+ # Set the state as if it were a hash.
25
+ #
26
+ # @param key [String, Symbol]
27
+ # @param value [Array, Hash, Number, String]
28
+ def []=(key, value)
29
+ @hash[key.to_s] = value
30
+ end
31
+
32
+ # Loads persisted state.
33
+ def load
34
+ @hash =
35
+ if File.exist?(file_path) && yaml = YAML.load_file(file_path)
36
+ yaml.to_hash
37
+ else
38
+ {} # Handle empty files
39
+ end
40
+ end
41
+
42
+ # Persists state to disk.
43
+ def save
44
+ File.open(file_path, 'w') { |f| f.write(@hash.to_yaml) }
45
+ end
46
+
47
+ # Destroy persisted state.
48
+ def destroy
49
+ @hash = {}
50
+ FileUtils.rm_f(file_path)
51
+ end
52
+
53
+ private
54
+
55
+ def file_path
56
+ File.join(@suite.storage_directory, FILE_NAME)
57
+ end
58
+ end
59
+ end
data/lib/navo/utils.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'digest'
2
+ require 'pathname'
3
+
4
+ module Navo
5
+ # A miscellaneous set of utility functions.
6
+ module Utils
7
+ module_function
8
+
9
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
10
+ #
11
+ # @param [String] string
12
+ # @return [String]
13
+ def camel_case(string)
14
+ string.split(/_|-| /)
15
+ .map { |part| part.sub(/^\w/) { |c| c.upcase } }
16
+ .join
17
+ end
18
+
19
+ # Returns the given {Time} as a human-readable string based on that time
20
+ # relative to the current time.
21
+ #
22
+ # @param time [Time]
23
+ # @return [String]
24
+ def human_time(time)
25
+ date = time.to_date
26
+
27
+ if date == Date.today
28
+ time.strftime('%l:%M %p')
29
+ elsif date == Date.today - 1
30
+ 'Yesterday'
31
+ elsif date > Date.today - 7
32
+ time.strftime('%A') # Sunday
33
+ elsif date.year == Date.today.year
34
+ time.strftime('%b %e') # Jun 22
35
+ else
36
+ time.strftime('%b %e, %Y') # Jun 22, 2015
37
+ end
38
+ end
39
+
40
+ # Convert string containing camel case or spaces into snake case.
41
+ #
42
+ # @see stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
43
+ #
44
+ # @param [String] string
45
+ # @return [String]
46
+ def snake_case(string)
47
+ string.gsub(/::/, '/')
48
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
49
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
50
+ .tr('-', '_')
51
+ .downcase
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ # Defines the gem version.
2
+ module Navo
3
+ VERSION = '0.1.0'
4
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: navo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane da Silva
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: berkshelf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: docker-api
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.22'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.22'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Test Chef cookbooks in Docker containers
56
+ email:
57
+ - shane@dasilva.io
58
+ executables:
59
+ - navo
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - bin/navo
64
+ - lib/navo.rb
65
+ - lib/navo/cli.rb
66
+ - lib/navo/configuration.rb
67
+ - lib/navo/constants.rb
68
+ - lib/navo/errors.rb
69
+ - lib/navo/sandbox.rb
70
+ - lib/navo/suite.rb
71
+ - lib/navo/suite_state.rb
72
+ - lib/navo/utils.rb
73
+ - lib/navo/version.rb
74
+ homepage: https://github.com/sds/navo
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 2.0.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.4.5.1
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Test Chef cookbooks in Docker containers
98
+ test_files: []
99
+ has_rdoc: