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 +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:
|