gene_system 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.doxie.json +25 -0
- data/.github/workflows/build.yml +25 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +20 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +51 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +150 -0
- data/LICENSE +21 -0
- data/README.md +255 -0
- data/Rakefile +19 -0
- data/bin/genesystem +52 -0
- data/data/manifest.jsonnet +87 -0
- data/gene_system.gemspec +40 -0
- data/lib/gene_system.rb +9 -0
- data/lib/gene_system/cli.rb +76 -0
- data/lib/gene_system/cli/commands.rb +109 -0
- data/lib/gene_system/generators.rb +58 -0
- data/lib/gene_system/manifest.rb +127 -0
- data/lib/gene_system/platform.rb +55 -0
- data/lib/gene_system/step.rb +74 -0
- data/lib/gene_system/version.rb +3 -0
- data/spec/gene_system/cli/commands_spec.rb +230 -0
- data/spec/gene_system/cli_spec.rb +83 -0
- data/spec/gene_system/generators_spec.rb +65 -0
- data/spec/gene_system/manifest_spec.rb +239 -0
- data/spec/gene_system/platform_spec.rb +94 -0
- data/spec/gene_system/step_spec.rb +108 -0
- data/spec/spec_helper.rb +27 -0
- metadata +344 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'jsonnet'
|
2
|
+
require 'hashie'
|
3
|
+
|
4
|
+
module GeneSystem
|
5
|
+
# Manifest is an in memory representation of a manifest file
|
6
|
+
class Manifest
|
7
|
+
DEFAULT_QUERY = ->(_step) { return true }
|
8
|
+
|
9
|
+
class <<self
|
10
|
+
##
|
11
|
+
# Creates a [GeneSystem::Manifest] from a manifest json so long as the
|
12
|
+
# manifest is compatible with this version of GeneSystem.
|
13
|
+
#
|
14
|
+
# @param [String] file_path
|
15
|
+
#
|
16
|
+
def new_from_file(file_path)
|
17
|
+
manifest = Jsonnet.evaluate(
|
18
|
+
File.read(file_path)
|
19
|
+
)
|
20
|
+
|
21
|
+
if incompatible?(manifest)
|
22
|
+
raise 'provided manifest is invalid or incompatible with '\
|
23
|
+
'this version of gene_system'
|
24
|
+
end
|
25
|
+
|
26
|
+
new(
|
27
|
+
file_path,
|
28
|
+
manifest
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Incompatible returns true if the current manifest is not compatible
|
34
|
+
# with this version of GeneSystem.
|
35
|
+
#
|
36
|
+
# A manifest is not compatible if it was created with a version greater
|
37
|
+
# than this the installed version.
|
38
|
+
#
|
39
|
+
# @param [Hash] manifest
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
#
|
43
|
+
def incompatible?(manifest)
|
44
|
+
manifest_version = manifest['metadata']['gene_system']['version']
|
45
|
+
manifest_version > GeneSystem::VERSION
|
46
|
+
rescue NoMethodError
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# list of supported platforms
|
52
|
+
SUPPORTED_PLATFORMS = %w[macos debian].freeze
|
53
|
+
|
54
|
+
def initialize(path, data)
|
55
|
+
@path = path
|
56
|
+
@data = Hashie::Mash.new(data)
|
57
|
+
@steps = GeneSystem::Step.load_steps(@data.steps)
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Manifest name getter
|
62
|
+
#
|
63
|
+
# @return [String]
|
64
|
+
#
|
65
|
+
def name
|
66
|
+
@data.name
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Manifest version getter
|
71
|
+
#
|
72
|
+
# @return [String]
|
73
|
+
#
|
74
|
+
def version
|
75
|
+
@data.version
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Manifest metadata getter
|
80
|
+
#
|
81
|
+
# @return[String]
|
82
|
+
#
|
83
|
+
def metadata
|
84
|
+
@data.metadata
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Platform metadata getter
|
89
|
+
#
|
90
|
+
# Prints a warning when the platform is not recognized
|
91
|
+
#
|
92
|
+
# @return[String]
|
93
|
+
#
|
94
|
+
def platform
|
95
|
+
platform = @data.platform
|
96
|
+
|
97
|
+
unless SUPPORTED_PLATFORMS.include?(platform)
|
98
|
+
CLI.print_warning("WARN: unrecognized platform: #{@data.platform}")
|
99
|
+
end
|
100
|
+
|
101
|
+
platform
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Steps executes a query function in a select call against each step to
|
106
|
+
# return a list of steps relevant to an operation.
|
107
|
+
#
|
108
|
+
# The given query function should evaluate to true when the desired step
|
109
|
+
# should be in the return set.
|
110
|
+
#
|
111
|
+
# By default a all steps will be returned.
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# query = ->(step) { step.tags.include?("foo") }
|
115
|
+
# manifest.steps(query)
|
116
|
+
#
|
117
|
+
# @param [Lambda] query
|
118
|
+
#
|
119
|
+
# @return [Array]
|
120
|
+
#
|
121
|
+
def steps(query = DEFAULT_QUERY)
|
122
|
+
@steps.select do |step|
|
123
|
+
query.call(step)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'os'
|
2
|
+
require 'ruby-handlebars'
|
3
|
+
|
4
|
+
module GeneSystem
|
5
|
+
# Platform is a class to handle command execution on host system
|
6
|
+
class Platform
|
7
|
+
##
|
8
|
+
# Platform constructor. Raises unsupported platform RuntimeError
|
9
|
+
# when the host system is not a posix system.
|
10
|
+
#
|
11
|
+
def initialize
|
12
|
+
raise 'unsupported platform' unless posix?
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Takes an array of commands for the host and executes each one
|
17
|
+
#
|
18
|
+
# If command returns non zero status code, then a command failed
|
19
|
+
# RuntimeError will be raised
|
20
|
+
#
|
21
|
+
# @param[Array] cmds
|
22
|
+
#
|
23
|
+
def execute_commands(cmds = [], vars = {})
|
24
|
+
cmds.each do |cmd|
|
25
|
+
status = execute_command(cmd, vars)
|
26
|
+
raise "command `#{cmd}` failed - returned #{status}" unless status.zero?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Executes a command on a host system and returns command exit code
|
32
|
+
#
|
33
|
+
# @param [String] cmd
|
34
|
+
#
|
35
|
+
# @return [Integer]
|
36
|
+
def execute_command(cmd, vars = {})
|
37
|
+
hbs = Handlebars::Handlebars.new
|
38
|
+
|
39
|
+
pid = Process.spawn(hbs.compile(cmd).call(vars))
|
40
|
+
_, status = Process.waitpid2(pid)
|
41
|
+
|
42
|
+
status.exitstatus
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Posix platform check. Returns true if host is a posix system
|
47
|
+
# i.e. Mac/Linux etc.
|
48
|
+
#
|
49
|
+
# @return [Boolean]
|
50
|
+
#
|
51
|
+
def posix?
|
52
|
+
OS.posix?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module GeneSystem
|
4
|
+
# Step is an in memory representation of a manifest step
|
5
|
+
class Step
|
6
|
+
class <<self
|
7
|
+
##
|
8
|
+
# Loads steps from an array of steps
|
9
|
+
#
|
10
|
+
# @param [Array] steps
|
11
|
+
#
|
12
|
+
# @return [Array]
|
13
|
+
#
|
14
|
+
def load_steps(steps)
|
15
|
+
steps.map do |data|
|
16
|
+
new(data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :tags
|
22
|
+
|
23
|
+
def initialize(data)
|
24
|
+
@data = Hashie::Mash.new(data)
|
25
|
+
@tags = []
|
26
|
+
@tags = @data.tags.split("\s") if @data.tags
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Step name getter
|
31
|
+
#
|
32
|
+
# @return [String]
|
33
|
+
#
|
34
|
+
def name
|
35
|
+
@data.name
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Step prompt getter
|
40
|
+
#
|
41
|
+
# @return [Array]
|
42
|
+
#
|
43
|
+
def prompts
|
44
|
+
@data.prompts
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Step execution instructions getter
|
49
|
+
#
|
50
|
+
# @return [Hash]
|
51
|
+
#
|
52
|
+
def exe
|
53
|
+
@data.exe
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Installation instructions getter
|
58
|
+
#
|
59
|
+
# @return [Array]
|
60
|
+
#
|
61
|
+
def install
|
62
|
+
exe.install
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Removal instructions getter
|
67
|
+
#
|
68
|
+
# @return [Array]
|
69
|
+
#
|
70
|
+
def remove
|
71
|
+
exe.remove
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe GeneSystem::CLI::Commands do
|
4
|
+
describe '.new' do
|
5
|
+
let(:pwd) { 'path/to/current/dir' }
|
6
|
+
let(:name) { 'manifest name' }
|
7
|
+
let(:args) { [name] }
|
8
|
+
|
9
|
+
before do
|
10
|
+
allow(Dir).to receive(:pwd).and_return(pwd)
|
11
|
+
allow(GeneSystem::Generators).to receive(:render_empty_manifest)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'renders empty manifest at current path with given name' do
|
15
|
+
described_class.new(args)
|
16
|
+
|
17
|
+
expect(GeneSystem::Generators).to have_received(:render_empty_manifest)
|
18
|
+
.with(name, pwd)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when name is not given' do
|
22
|
+
it 'raises no manifest name provided runtime error' do
|
23
|
+
expect { described_class.new([]) }.to raise_error(
|
24
|
+
RuntimeError,
|
25
|
+
'no manifest name provided'
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.install' do
|
32
|
+
let(:manifest_name) { 'manifest.json' }
|
33
|
+
let(:pwd) { 'path/to/current/dir' }
|
34
|
+
|
35
|
+
let(:args) { [manifest_name] }
|
36
|
+
let(:file_exist) { true }
|
37
|
+
|
38
|
+
let(:skip_cmd) { 'check something' }
|
39
|
+
let(:skip_result) { 1 }
|
40
|
+
|
41
|
+
let(:cmd) { 'do something' }
|
42
|
+
let(:cmds) { [cmd, cmd] }
|
43
|
+
|
44
|
+
let(:data) do
|
45
|
+
{
|
46
|
+
'name' => 'test_manifest',
|
47
|
+
'version' => '0.1.0',
|
48
|
+
'steps' => [
|
49
|
+
{
|
50
|
+
'name' => 'install',
|
51
|
+
'exe' => {
|
52
|
+
'install' => {
|
53
|
+
'skip' => skip_cmd,
|
54
|
+
'cmd' => cmds
|
55
|
+
}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
]
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
let(:manifest) do
|
63
|
+
GeneSystem::Manifest.new(
|
64
|
+
File.join(pwd, manifest_name),
|
65
|
+
data
|
66
|
+
)
|
67
|
+
end
|
68
|
+
let(:platform) { double(GeneSystem::Platform) }
|
69
|
+
|
70
|
+
before do
|
71
|
+
allow(Dir).to receive(:pwd).and_return(pwd)
|
72
|
+
allow(File).to receive(:exist?).and_return(file_exist)
|
73
|
+
|
74
|
+
allow(GeneSystem::Manifest).to receive(:new_from_file)
|
75
|
+
.and_return(manifest)
|
76
|
+
|
77
|
+
allow(GeneSystem::Platform).to receive(:new)
|
78
|
+
.and_return(platform)
|
79
|
+
|
80
|
+
allow(platform).to receive(:execute_commands)
|
81
|
+
allow(platform).to receive(:execute_command)
|
82
|
+
.with(skip_cmd)
|
83
|
+
.and_return(skip_result)
|
84
|
+
|
85
|
+
allow(GeneSystem::CLI).to receive(:print_message)
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'installing manifest' do
|
89
|
+
before { described_class.install(args) }
|
90
|
+
|
91
|
+
it 'loads manifest' do
|
92
|
+
expect(GeneSystem::Manifest)
|
93
|
+
.to have_received(:new_from_file)
|
94
|
+
.with(File.join(pwd, manifest_name))
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'executes steps on platform' do
|
98
|
+
expect(platform)
|
99
|
+
.to have_received(:execute_commands)
|
100
|
+
.with(cmds, {})
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'prints success message' do
|
104
|
+
expect(GeneSystem::CLI).to have_received(:print_message)
|
105
|
+
.with("\nmanifest successfully installed")
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when skipping step' do
|
109
|
+
let(:skip_result) { 0 }
|
110
|
+
|
111
|
+
it 'does not execute steps' do
|
112
|
+
expect(platform)
|
113
|
+
.not_to have_received(:execute_commands)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'when path is not provided' do
|
119
|
+
it 'raises no manifest path provided runtime error' do
|
120
|
+
expect { described_class.install }.to raise_error(
|
121
|
+
RuntimeError,
|
122
|
+
'no manifest path provided'
|
123
|
+
)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'when manifest cannot be created at specified path' do
|
128
|
+
let(:file_exist) { false }
|
129
|
+
|
130
|
+
it 'raises cannot find manifest RuntimeError' do
|
131
|
+
expect { described_class.install(args) }.to raise_error(
|
132
|
+
RuntimeError,
|
133
|
+
'cannot find manifest at path/to/current/dir/manifest.json'
|
134
|
+
)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe '.remove' do
|
140
|
+
let(:manifest_name) { 'manifest.json' }
|
141
|
+
let(:pwd) { 'path/to/current/dir' }
|
142
|
+
|
143
|
+
let(:args) { [manifest_name] }
|
144
|
+
let(:file_exist) { true }
|
145
|
+
|
146
|
+
let(:cmd) { 'do something' }
|
147
|
+
let(:cmds) { [cmd, cmd] }
|
148
|
+
|
149
|
+
let(:data) do
|
150
|
+
{
|
151
|
+
'name' => 'test_manifest',
|
152
|
+
'version' => '0.1.0',
|
153
|
+
'steps' => [
|
154
|
+
{
|
155
|
+
'name' => 'remove',
|
156
|
+
'exe' => {
|
157
|
+
'remove' => {
|
158
|
+
'cmd' => cmds
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
]
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
let(:manifest) do
|
167
|
+
GeneSystem::Manifest.new(
|
168
|
+
File.join(pwd, manifest_name),
|
169
|
+
data
|
170
|
+
)
|
171
|
+
end
|
172
|
+
let(:platform) { double(GeneSystem::Platform) }
|
173
|
+
|
174
|
+
before do
|
175
|
+
allow(Dir).to receive(:pwd).and_return(pwd)
|
176
|
+
allow(File).to receive(:exist?).and_return(file_exist)
|
177
|
+
|
178
|
+
allow(GeneSystem::Manifest).to receive(:new_from_file)
|
179
|
+
.and_return(manifest)
|
180
|
+
|
181
|
+
allow(GeneSystem::Platform).to receive(:new)
|
182
|
+
.and_return(platform)
|
183
|
+
|
184
|
+
allow(platform).to receive(:execute_commands)
|
185
|
+
|
186
|
+
allow(GeneSystem::CLI).to receive(:print_message)
|
187
|
+
end
|
188
|
+
|
189
|
+
describe 'remooving manifest' do
|
190
|
+
before { described_class.remove(args) }
|
191
|
+
|
192
|
+
it 'loads manifest' do
|
193
|
+
expect(GeneSystem::Manifest)
|
194
|
+
.to have_received(:new_from_file)
|
195
|
+
.with(File.join(pwd, manifest_name))
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'executes remove on platform' do
|
199
|
+
expect(platform)
|
200
|
+
.to have_received(:execute_commands)
|
201
|
+
.with(cmds)
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'prints success message' do
|
205
|
+
expect(GeneSystem::CLI).to have_received(:print_message)
|
206
|
+
.with("\nmanifest successfully removed")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'when path is not provided' do
|
211
|
+
it 'raises no manifest path provided runtime error' do
|
212
|
+
expect { described_class.install }.to raise_error(
|
213
|
+
RuntimeError,
|
214
|
+
'no manifest path provided'
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context 'when manifest cannot be created at specified path' do
|
220
|
+
let(:file_exist) { false }
|
221
|
+
|
222
|
+
it 'raises cannot find manifest RuntimeError' do
|
223
|
+
expect { described_class.install(args) }.to raise_error(
|
224
|
+
RuntimeError,
|
225
|
+
'cannot find manifest at path/to/current/dir/manifest.json'
|
226
|
+
)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|