navo 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/navo +5 -0
- data/lib/navo.rb +8 -0
- data/lib/navo/cli.rb +59 -0
- data/lib/navo/configuration.rb +102 -0
- data/lib/navo/constants.rb +7 -0
- data/lib/navo/errors.rb +14 -0
- data/lib/navo/sandbox.rb +107 -0
- data/lib/navo/suite.rb +230 -0
- data/lib/navo/suite_state.rb +59 -0
- data/lib/navo/utils.rb +54 -0
- data/lib/navo/version.rb +4 -0
- metadata +99 -0
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
data/lib/navo.rb
ADDED
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
|
data/lib/navo/errors.rb
ADDED
@@ -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
|
data/lib/navo/sandbox.rb
ADDED
@@ -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
|
data/lib/navo/version.rb
ADDED
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:
|