orca 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.
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +29 -0
- data/LICENSE +7 -0
- data/README.md +109 -0
- data/Rakefile +9 -0
- data/bin/orca +6 -0
- data/config/template/files/.empty_directory +0 -0
- data/config/template/orca.rb +28 -0
- data/lib/orca.rb +48 -0
- data/lib/orca/cli.rb +53 -0
- data/lib/orca/dsl.rb +18 -0
- data/lib/orca/execution_context.rb +89 -0
- data/lib/orca/extensions/apt.rb +54 -0
- data/lib/orca/extensions/file_sync.rb +100 -0
- data/lib/orca/local_file.rb +77 -0
- data/lib/orca/node.rb +83 -0
- data/lib/orca/package.rb +52 -0
- data/lib/orca/package_index.rb +37 -0
- data/lib/orca/remote_file.rb +104 -0
- data/lib/orca/resolver.rb +37 -0
- data/lib/orca/runner.rb +81 -0
- data/lib/orca/suite.rb +18 -0
- data/orca.gemspec +18 -0
- data/test/dsl_test.rb +35 -0
- data/test/fixtures/example.txt +1 -0
- data/test/local_file_test.rb +59 -0
- data/test/package_index_test.rb +48 -0
- data/test/package_test.rb +51 -0
- data/test/remote_file_test.rb +91 -0
- data/test/resolver_test.rb +61 -0
- data/test/test_helper.rb +8 -0
- metadata +150 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
class Orca::Resolver
|
2
|
+
attr_reader :packages, :tree
|
3
|
+
def initialize(package)
|
4
|
+
@package = package
|
5
|
+
@last_seen = package
|
6
|
+
@tree = [@package]
|
7
|
+
@packages = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def resolve
|
11
|
+
dependancies = @package.dependancies.reverse.map { |d| Orca::PackageIndex.default.get(d) }
|
12
|
+
begin
|
13
|
+
@tree += dependancies.map {|d| Orca::Resolver.new(d).resolve.tree }
|
14
|
+
rescue SystemStackError
|
15
|
+
raise CircularDependancyError.new
|
16
|
+
end
|
17
|
+
@packages = @tree.flatten
|
18
|
+
@packages.reverse!
|
19
|
+
@packages.uniq!
|
20
|
+
add_children
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_children
|
25
|
+
@packages = @packages.reduce([]) do |arr, package|
|
26
|
+
arr << package
|
27
|
+
package.children.each do |child_name|
|
28
|
+
child = Orca::PackageIndex.default.get(child_name)
|
29
|
+
arr << child unless arr.include?(child)
|
30
|
+
end
|
31
|
+
arr
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class CircularDependancyError < StandardError
|
36
|
+
end
|
37
|
+
end
|
data/lib/orca/runner.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
class Orca::Runner
|
2
|
+
def initialize(node, package)
|
3
|
+
@node = node
|
4
|
+
@package = package
|
5
|
+
@perform = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def packages
|
9
|
+
resolver = Orca::Resolver.new(@package)
|
10
|
+
resolver.resolve
|
11
|
+
resolver.packages
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(command_name)
|
15
|
+
@node.log command_name, packages.map(&:name).join(', ').yellow
|
16
|
+
packages.each do |pkg|
|
17
|
+
send(:"execute_#{command_name}", pkg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute_apply(pkg)
|
22
|
+
return unless should_run?(pkg, :apply)
|
23
|
+
exec(pkg, :apply)
|
24
|
+
validate!(pkg)
|
25
|
+
end
|
26
|
+
|
27
|
+
def execute_remove(pkg)
|
28
|
+
return unless should_run?(pkg, :remove)
|
29
|
+
exec(pkg, :remove)
|
30
|
+
end
|
31
|
+
|
32
|
+
def execute_validate(pkg)
|
33
|
+
validate!(pkg)
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_run?(pkg, command_name)
|
37
|
+
return false unless pkg.provides_command?(command_name)
|
38
|
+
return true unless @perform
|
39
|
+
return true unless command_name == :apply || command_name == :remove
|
40
|
+
return true unless pkg.provides_command?(:validate)
|
41
|
+
is_present = is_valid?(pkg)
|
42
|
+
return !is_present if command_name == :apply
|
43
|
+
return is_present
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate!(pkg)
|
47
|
+
return true unless @perform
|
48
|
+
return unless pkg.provides_command?(:validate)
|
49
|
+
return if is_valid?(pkg)
|
50
|
+
raise ValidationFailureError.new(@node, pkg)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_valid?(pkg)
|
54
|
+
results = exec(pkg, :validate)
|
55
|
+
results.all?
|
56
|
+
end
|
57
|
+
|
58
|
+
def demonstrate(command_name)
|
59
|
+
@perform = false
|
60
|
+
execute(command_name)
|
61
|
+
@perform = true
|
62
|
+
end
|
63
|
+
|
64
|
+
def exec(pkg, command_name)
|
65
|
+
@node.log pkg.name, command_name.to_s.yellow
|
66
|
+
context = @perform ? Orca::ExecutionContext.new(@node) : Orca::MockExecutionContext.new(@node)
|
67
|
+
cmds = pkg.command(command_name)
|
68
|
+
cmds.map {|cmd| context.apply(cmd) }
|
69
|
+
end
|
70
|
+
|
71
|
+
class ValidationFailureError < StandardError
|
72
|
+
def initialize(node, package)
|
73
|
+
@node = node
|
74
|
+
@package = package
|
75
|
+
end
|
76
|
+
|
77
|
+
def message
|
78
|
+
"Package #{@package.name} failed validation on #{@node.to_s}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/orca/suite.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class Orca::Suite
|
2
|
+
|
3
|
+
def load_file(file)
|
4
|
+
Orca::DSL.module_eval(File.read(file))
|
5
|
+
end
|
6
|
+
|
7
|
+
def execute(node_name, pkg_name, command)
|
8
|
+
node = Orca::Node.find(node_name)
|
9
|
+
pkg = Orca::PackageIndex.default.get(pkg_name)
|
10
|
+
Orca::Runner.new(node, pkg).execute(command)
|
11
|
+
end
|
12
|
+
|
13
|
+
def demonstrate(node_name, pkg_name, command)
|
14
|
+
node = Orca::Node.find(node_name)
|
15
|
+
pkg = Orca::PackageIndex.default.get(pkg_name)
|
16
|
+
Orca::Runner.new(node, pkg).demonstrate(command)
|
17
|
+
end
|
18
|
+
end
|
data/orca.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.authors = ["Andy Kent"]
|
3
|
+
gem.email = ["andy.kent@me.com"]
|
4
|
+
gem.description = %q{Orca is a super simple way to build and configure servers}
|
5
|
+
gem.summary = %q{Simplified Machine Building}
|
6
|
+
gem.homepage = ""
|
7
|
+
|
8
|
+
gem.files = `git ls-files`.split($\)
|
9
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
10
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
11
|
+
gem.name = "orca"
|
12
|
+
gem.require_paths = ["lib"]
|
13
|
+
gem.version = '0.1.0'
|
14
|
+
gem.add_dependency('colored')
|
15
|
+
gem.add_dependency('net-ssh')
|
16
|
+
gem.add_dependency('net-sftp')
|
17
|
+
gem.add_dependency('thor')
|
18
|
+
end
|
data/test/dsl_test.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::DSL do
|
4
|
+
describe "'package' command" do
|
5
|
+
after :each do
|
6
|
+
reset_package_index!
|
7
|
+
end
|
8
|
+
|
9
|
+
it "adds a package to the index" do
|
10
|
+
Orca::DSL.package('my-package') { nil }
|
11
|
+
Orca::PackageIndex.default.get('my-package').must_be_instance_of Orca::Package
|
12
|
+
end
|
13
|
+
|
14
|
+
it "creates a new package based on the supplied name" do
|
15
|
+
Orca::DSL.package('my-package') { nil }
|
16
|
+
package = Orca::PackageIndex.default.get('my-package')
|
17
|
+
package.name.must_equal 'my-package'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "executes the given definition block in the package context" do
|
21
|
+
Orca::DSL.package('my-package') { depends_on 'other-package' }
|
22
|
+
package = Orca::PackageIndex.default.get('my-package')
|
23
|
+
package.dependancies.must_equal ['other-package']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "'node' command" do
|
28
|
+
it "creates a node and adds it to the index" do
|
29
|
+
Orca::DSL.node('node-name', 'node-host')
|
30
|
+
node = Orca::Node.find('node-name')
|
31
|
+
node.name.must_equal 'node-name'
|
32
|
+
node.host.must_equal 'node-host'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
example
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::LocalFile do
|
4
|
+
before :each do
|
5
|
+
@local_file_path = File.join(File.dirname(__FILE__), 'fixtures', 'example.txt')
|
6
|
+
@local_file = Orca::LocalFile.new(@local_file_path)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe 'path' do
|
10
|
+
it "returns the absolute path of a local file" do
|
11
|
+
@local_file.path.must_equal @local_file_path
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'hash' do
|
16
|
+
it "returns the sha1 of a local file" do
|
17
|
+
@local_file.hash.must_equal 'c3499c2729730a7f807efb8676a92dcb6f8a3f8f'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'matches' do
|
22
|
+
it "returns true if the passed file has a matching hash" do
|
23
|
+
@local_file.matches?(@local_file).must_equal true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'exists?' do
|
28
|
+
it "checks if a file exists" do
|
29
|
+
@local_file.exists?.must_equal true
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'copy_to' do
|
34
|
+
before(:each) { @destination = Orca::LocalFile.new("/tmp/example-#{Time.now.to_i}.txt") }
|
35
|
+
after(:each) { @destination.delete! }
|
36
|
+
|
37
|
+
it "copies a file to another local location" do
|
38
|
+
@destination.exists?.must_equal false
|
39
|
+
@local_file.copy_to(@destination).must_equal @destination
|
40
|
+
@destination.exists?.must_equal true
|
41
|
+
end
|
42
|
+
|
43
|
+
it "copies a file to a remote location by uploading it" do
|
44
|
+
@remote_destination_context = mock()
|
45
|
+
@remote_destination = Orca::RemoteFile.new(@remote_destination_context, "/tmp/example-dest.txt")
|
46
|
+
@remote_destination_context.expects(:upload).with(@local_file.path, @remote_destination.path)
|
47
|
+
@local_file.copy_to(@remote_destination).must_equal @remote_destination
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'set_permissions' do
|
52
|
+
it "sets permissions on the file based on a mask" do
|
53
|
+
@local_file.set_permissions(0664).must_equal @local_file
|
54
|
+
@local_file.permissions.must_equal 0664
|
55
|
+
@local_file.set_permissions(0644).must_equal @local_file
|
56
|
+
@local_file.permissions.must_equal 0644
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::PackageIndex do
|
4
|
+
before :each do
|
5
|
+
@package = Orca::Package.new('my-package')
|
6
|
+
@default = Orca::PackageIndex.default
|
7
|
+
end
|
8
|
+
|
9
|
+
after :each do
|
10
|
+
reset_package_index!
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "default" do
|
14
|
+
it "returns a package index singleton named default" do
|
15
|
+
Orca::PackageIndex.default.index_name.must_equal 'default'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "allways returns the same package index" do
|
19
|
+
Orca::PackageIndex.default.must_equal Orca::PackageIndex.default
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "add" do
|
24
|
+
it "adds a package to the index" do
|
25
|
+
@default.add(@package)
|
26
|
+
@default.get(@package.name).must_equal @package
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'get' do
|
31
|
+
it "fetches a package by name" do
|
32
|
+
@default.add(@package)
|
33
|
+
@default.get(@package.name).must_equal @package
|
34
|
+
end
|
35
|
+
|
36
|
+
it "throws an execption if the package doesn't exist" do
|
37
|
+
assert_raises(Orca::PackageIndex::MissingPackageError) { @default.get(@package.name) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "clear!" do
|
42
|
+
it "wipes the index clean of packages" do
|
43
|
+
@default.add(@package)
|
44
|
+
@default.clear!
|
45
|
+
assert_raises(Orca::PackageIndex::MissingPackageError) { @default.get(@package.name) }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::Package do
|
4
|
+
before :each do
|
5
|
+
@package = Orca::Package.new('my-package')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "depends_on" do
|
9
|
+
it "adds a dependancy" do
|
10
|
+
@package.dependancies.must_equal []
|
11
|
+
@package.depends_on('other-package')
|
12
|
+
@package.dependancies.must_equal ['other-package']
|
13
|
+
end
|
14
|
+
|
15
|
+
it "adds multiple dependancies at once" do
|
16
|
+
@package.dependancies.must_equal []
|
17
|
+
@package.depends_on('other-package', 'third-package')
|
18
|
+
@package.dependancies.must_equal ['other-package', 'third-package']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "action" do
|
23
|
+
it "allows defining actions with a name and a block" do
|
24
|
+
@package.action('my-action') { 'foo' }
|
25
|
+
@package.actions['my-action'].call.must_equal 'foo'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "commands" do
|
30
|
+
it "can add an 'apply' command" do
|
31
|
+
@package.apply { 'my-apply' }
|
32
|
+
@package.command(:apply).first.call.must_equal 'my-apply'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can add an 'remove' command" do
|
36
|
+
@package.remove { 'my-remove' }
|
37
|
+
@package.command(:remove).first.call.must_equal 'my-remove'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can add an 'validate' command" do
|
41
|
+
@package.validate { 'my-validate' }
|
42
|
+
@package.command(:validate).first.call.must_equal 'my-validate'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'knows if a command is provided' do
|
46
|
+
@package.provides_command?(:apply).must_equal false
|
47
|
+
@package.apply { 'my-apply' }
|
48
|
+
@package.provides_command?(:apply).must_equal true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
describe Orca::RemoteFile do
|
4
|
+
before :each do
|
5
|
+
@local_file_path = File.join(File.dirname(__FILE__), 'fixtures', 'example.txt')
|
6
|
+
@local_file = Orca::LocalFile.new(@local_file_path)
|
7
|
+
@remote_file_path = '/tmp/example.txt'
|
8
|
+
@context = mock()
|
9
|
+
@remote_file = Orca::RemoteFile.new(@context, @remote_file_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'path' do
|
13
|
+
it "returns the absolute path of a local file" do
|
14
|
+
@remote_file.path.must_equal @remote_file_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'hash' do
|
19
|
+
it "returns the sha1 of a remote file" do
|
20
|
+
@context.expects(:run)
|
21
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
22
|
+
.returns("true\n")
|
23
|
+
@context.expects(:run)
|
24
|
+
.with("sha1sum #{@remote_file_path}")
|
25
|
+
.returns("c3499c2729730a7f807efb8676a92dcb6f8a3f8f #{@remote_file_path}")
|
26
|
+
@remote_file.hash.must_equal 'c3499c2729730a7f807efb8676a92dcb6f8a3f8f'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'matches' do
|
31
|
+
it "returns true if the passed file has a matching hash" do
|
32
|
+
@context.expects(:run)
|
33
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
34
|
+
.returns("true\n")
|
35
|
+
@context.expects(:run)
|
36
|
+
.with("sha1sum #{@remote_file_path}")
|
37
|
+
.returns("c3499c2729730a7f807efb8676a92dcb6f8a3f8f #{@remote_file_path}")
|
38
|
+
@remote_file.matches?(@remote_file).must_equal true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'exists?' do
|
43
|
+
it "checks if a file exists" do
|
44
|
+
@context.expects(:run)
|
45
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
46
|
+
.returns("true\n")
|
47
|
+
@remote_file.exists?.must_equal true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "checks if a missing file exists" do
|
51
|
+
@context.expects(:run)
|
52
|
+
.with(%[if [ -f #{@remote_file_path} ]; then echo "true"; else echo "false"; fi])
|
53
|
+
.returns("false\n")
|
54
|
+
@remote_file.exists?.must_equal false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe 'copy_to' do
|
59
|
+
it "copies a file to another remote location" do
|
60
|
+
@remote_destination_context = mock()
|
61
|
+
@remote_destination = Orca::RemoteFile.new(@remote_destination_context, "/tmp/example-dest.txt")
|
62
|
+
@context.expects(:sudo)
|
63
|
+
.with(%[cp #{@remote_file.path} #{@remote_destination.path}])
|
64
|
+
.returns("true\n")
|
65
|
+
@remote_file.copy_to(@remote_destination).must_equal @remote_destination
|
66
|
+
end
|
67
|
+
|
68
|
+
it "copies a file to local location by downlaoding it" do
|
69
|
+
@local_destination = Orca::LocalFile.new("/tmp/example-#{Time.now.to_i}.txt")
|
70
|
+
@context.expects(:download)
|
71
|
+
.with(@remote_file_path, @local_destination.path)
|
72
|
+
@remote_file.copy_to(@local_destination).must_equal @local_destination
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'delete!' do
|
77
|
+
it "removes the file from the remote server" do
|
78
|
+
@context.expects(:remove).with(@remote_file.path)
|
79
|
+
@remote_file.delete!
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'set_permissions' do
|
84
|
+
it "sets permissions on the file based on a mask" do
|
85
|
+
@context.expects(:sudo).with('chmod -R 644 /tmp/example.txt')
|
86
|
+
@context.expects(:run).with("stat --format=%a /tmp/example.txt").returns("644\n")
|
87
|
+
@remote_file.set_permissions(0644).must_equal @remote_file
|
88
|
+
@remote_file.permissions.must_equal 0644
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|