navo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: