sprinkle 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/CREDITS +14 -0
  2. data/History.txt +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/Manifest.txt +63 -0
  5. data/README.rdoc +218 -0
  6. data/Rakefile +4 -0
  7. data/TODO +56 -0
  8. data/bin/sprinkle +79 -0
  9. data/config/hoe.rb +70 -0
  10. data/config/requirements.rb +17 -0
  11. data/examples/merb/deploy.rb +5 -0
  12. data/examples/rails/README +15 -0
  13. data/examples/rails/deploy.rb +2 -0
  14. data/examples/rails/packages/database.rb +9 -0
  15. data/examples/rails/packages/essential.rb +6 -0
  16. data/examples/rails/packages/rails.rb +28 -0
  17. data/examples/rails/packages/search.rb +11 -0
  18. data/examples/rails/packages/server.rb +28 -0
  19. data/examples/rails/rails.rb +71 -0
  20. data/examples/sprinkle/sprinkle.rb +38 -0
  21. data/lib/sprinkle/actors/capistrano.rb +80 -0
  22. data/lib/sprinkle/actors/vlad.rb +30 -0
  23. data/lib/sprinkle/deployment.rb +33 -0
  24. data/lib/sprinkle/extensions/arbitrary_options.rb +10 -0
  25. data/lib/sprinkle/extensions/array.rb +7 -0
  26. data/lib/sprinkle/extensions/blank_slate.rb +5 -0
  27. data/lib/sprinkle/extensions/dsl_accessor.rb +15 -0
  28. data/lib/sprinkle/extensions/string.rb +10 -0
  29. data/lib/sprinkle/extensions/symbol.rb +7 -0
  30. data/lib/sprinkle/installers/apt.rb +20 -0
  31. data/lib/sprinkle/installers/gem.rb +33 -0
  32. data/lib/sprinkle/installers/installer.rb +85 -0
  33. data/lib/sprinkle/installers/rake.rb +17 -0
  34. data/lib/sprinkle/installers/rpm.rb +20 -0
  35. data/lib/sprinkle/installers/source.rb +120 -0
  36. data/lib/sprinkle/package.rb +94 -0
  37. data/lib/sprinkle/policy.rb +85 -0
  38. data/lib/sprinkle/script.rb +13 -0
  39. data/lib/sprinkle/sequence.rb +21 -0
  40. data/lib/sprinkle/version.rb +9 -0
  41. data/lib/sprinkle.rb +26 -0
  42. data/script/destroy +14 -0
  43. data/script/generate +14 -0
  44. data/spec/spec.opts +1 -0
  45. data/spec/spec_helper.rb +17 -0
  46. data/spec/sprinkle/actors/capistrano_spec.rb +150 -0
  47. data/spec/sprinkle/deployment_spec.rb +80 -0
  48. data/spec/sprinkle/extensions/array_spec.rb +19 -0
  49. data/spec/sprinkle/extensions/string_spec.rb +21 -0
  50. data/spec/sprinkle/installers/apt_spec.rb +53 -0
  51. data/spec/sprinkle/installers/gem_spec.rb +75 -0
  52. data/spec/sprinkle/installers/installer_spec.rb +125 -0
  53. data/spec/sprinkle/installers/rpm_spec.rb +50 -0
  54. data/spec/sprinkle/installers/source_spec.rb +315 -0
  55. data/spec/sprinkle/package_spec.rb +247 -0
  56. data/spec/sprinkle/policy_spec.rb +126 -0
  57. data/spec/sprinkle/script_spec.rb +51 -0
  58. data/spec/sprinkle/sequence_spec.rb +44 -0
  59. data/spec/sprinkle/sprinkle_spec.rb +25 -0
  60. data/sprinkle.gemspec +43 -0
  61. data/tasks/deployment.rake +34 -0
  62. data/tasks/environment.rake +7 -0
  63. data/tasks/rspec.rake +21 -0
  64. metadata +157 -0
@@ -0,0 +1,94 @@
1
+ module Sprinkle
2
+ module Package
3
+ PACKAGES = {}
4
+
5
+ def package(name, metadata = {}, &block)
6
+ package = Package.new(name, metadata, &block)
7
+ PACKAGES[name] = package
8
+
9
+ if package.provides
10
+ (PACKAGES[package.provides] ||= []) << package
11
+ end
12
+
13
+ package
14
+ end
15
+
16
+ class Package
17
+ include ArbitraryOptions
18
+ attr_accessor :name, :provides, :installer, :dependencies, :recommends
19
+
20
+ def initialize(name, metadata = {}, &block)
21
+ raise 'No package name supplied' unless name
22
+
23
+ @name = name
24
+ @provides = metadata[:provides]
25
+ @dependencies = []
26
+ @recommends = []
27
+ self.instance_eval &block
28
+ end
29
+
30
+ def apt(*names)
31
+ @installer = Sprinkle::Installers::Apt.new(self, *names)
32
+ end
33
+
34
+ def rpm(*names)
35
+ @installer = Sprinkle::Installers::Rpm.new(self, *names)
36
+ end
37
+
38
+ def gem(name, options = {}, &block)
39
+ @recommends << :rubygems
40
+ @installer = Sprinkle::Installers::Gem.new(self, name, options, &block)
41
+ end
42
+
43
+ def source(source, options = {}, &block)
44
+ @recommends << :build_essential # Ubuntu/Debian
45
+ @installer = Sprinkle::Installers::Source.new(self, source, options, &block)
46
+ end
47
+
48
+ def process(deployment, roles)
49
+ return if meta_package?
50
+
51
+ @installer.defaults(deployment)
52
+ @installer.process(roles)
53
+ end
54
+
55
+ def requires(*packages)
56
+ @dependencies << packages
57
+ @dependencies.flatten!
58
+ end
59
+
60
+ def recommends(*packages)
61
+ @recommends << packages
62
+ @recommends.flatten!
63
+ end
64
+
65
+ def tree(depth = 1, &block)
66
+ packages = []
67
+
68
+ @recommends.each do |dep|
69
+ package = PACKAGES[dep]
70
+ next unless package # skip missing recommended packages as they can be optional
71
+ block.call(self, package, depth) if block
72
+ packages << package.tree(depth + 1, &block)
73
+ end
74
+
75
+ @dependencies.each do |dep|
76
+ package = PACKAGES[dep]
77
+ raise "Package definition not found for key: #{dep}" unless package
78
+ block.call(self, package, depth) if block
79
+ packages << package.tree(depth + 1, &block)
80
+ end
81
+
82
+ packages << self
83
+ end
84
+
85
+ def to_s; @name; end
86
+
87
+ private
88
+
89
+ def meta_package?
90
+ @installer == nil
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,85 @@
1
+ require 'highline/import'
2
+
3
+ module Sprinkle
4
+ module Policy
5
+ POLICIES = []
6
+
7
+ def policy(name, options = {}, &block)
8
+ p = Policy.new(name, options, &block)
9
+ POLICIES << p
10
+ p
11
+ end
12
+
13
+ class Policy
14
+ attr_reader :name, :packages
15
+
16
+ def initialize(name, metadata = {}, &block)
17
+ raise 'No name provided' unless name
18
+ raise 'No roles provided' unless metadata[:roles]
19
+
20
+ @name = name
21
+ @roles = metadata[:roles]
22
+ @packages = []
23
+ self.instance_eval(&block)
24
+ end
25
+
26
+ def requires(package, options = {})
27
+ @packages << package
28
+ end
29
+
30
+ def to_s; name; end
31
+
32
+ def process(deployment)
33
+ all = []
34
+
35
+ cloud_info "--> Cloud hierarchy for policy #{@name}"
36
+
37
+ @packages.each do |p|
38
+ cloud_info "\nPolicy #{@name} requires package #{p}"
39
+
40
+ package = Sprinkle::Package::PACKAGES[p]
41
+ raise "Package definition not found for key: #{p}" unless package
42
+ package = select_package(p, package) if package.is_a? Array # handle virtual package selection
43
+
44
+ tree = package.tree do |parent, child, depth|
45
+ indent = "\t" * depth; cloud_info "#{indent}Package #{parent.name} requires #{child.name}"
46
+ end
47
+
48
+ all << tree
49
+ end
50
+
51
+ normalize(all) do |package|
52
+ package.process(deployment, @roles)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def cloud_info(message)
59
+ logger.info(message) if Sprinkle::OPTIONS[:cloud] or logger.debug?
60
+ end
61
+
62
+ def select_package(name, packages)
63
+ if packages.size <= 1
64
+ package = packages.first
65
+ else
66
+ package = choose do |menu|
67
+ menu.prompt = "Multiple choices exist for virtual package #{name}"
68
+ menu.choices *packages.collect(&:to_s)
69
+ end
70
+ package = Sprinkle::Package::PACKAGES[package]
71
+ end
72
+
73
+ cloud_info "Selecting #{package.to_s} for virtual package #{name}"
74
+
75
+ package
76
+ end
77
+
78
+ def normalize(all, &block)
79
+ all = all.flatten.uniq
80
+ cloud_info "\n--> Normalized installation order for all packages: #{all.collect(&:name).join(', ')}"
81
+ all.each &block
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,13 @@
1
+ module Sprinkle
2
+ class Script
3
+ def self.sprinkle(script, filename = '__SCRIPT__')
4
+ powder = new
5
+ powder.instance_eval script, filename
6
+ powder.sprinkle
7
+ end
8
+
9
+ def sprinkle
10
+ @deployment.process if @deployment
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Sprinkle
2
+ class Sequence
3
+ attr_accessor :stages
4
+
5
+ def initialize(&block)
6
+ self.instance_eval &block if block
7
+ end
8
+
9
+ def method_missing(sym, *args, &block)
10
+ @stages ||= ActiveSupport::OrderedHash.new
11
+ @stages[sym] = block
12
+ end
13
+
14
+ def each(&block)
15
+ @stages.each do |section, commands|
16
+ block.call section, commands.call
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module Sprinkle #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 4
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
data/lib/sprinkle.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+
4
+ # Use active supports auto load mechanism
5
+ Dependencies.load_paths << File.dirname(__FILE__)
6
+
7
+ # Configure active support to log auto-loading of dependencies
8
+ #Dependencies::RAILS_DEFAULT_LOGGER = Logger.new($stdout)
9
+ #Dependencies.log_activity = true
10
+
11
+ # Load up extensions to existing classes
12
+ Dir[File.dirname(__FILE__) + '/sprinkle/extensions/*.rb'].each { |e| require e }
13
+
14
+ # Configuration options
15
+ module Sprinkle
16
+ OPTIONS = { :testing => false, :verbose => false }
17
+ end
18
+
19
+ # Define a logging target and understand packages, policies and deployment DSL
20
+ class Object
21
+ include Sprinkle::Package, Sprinkle::Policy, Sprinkle::Deployment
22
+
23
+ def logger
24
+ @@__log__ ||= ActiveSupport::BufferedLogger.new($stdout, ActiveSupport::BufferedLogger::Severity::INFO)
25
+ end
26
+ end
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'sprinkle'
11
+
12
+ module Kernel
13
+ def logger
14
+ @@__log_file__ ||= StringIO.new
15
+ @@__log__ = ActiveSupport::BufferedLogger.new @@__log_file__
16
+ end
17
+ end
@@ -0,0 +1,150 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Sprinkle::Actors::Capistrano do
4
+
5
+ before do
6
+ @recipes = 'deploy'
7
+ @cap = ::Capistrano::Configuration.new
8
+ ::Capistrano::Configuration.stub!(:new).and_return(@cap)
9
+ @cap.stub!(:load).and_return
10
+ end
11
+
12
+ def create_cap(&block)
13
+ Sprinkle::Actors::Capistrano.new &block
14
+ end
15
+
16
+ describe 'when created' do
17
+
18
+ it 'should create a new capistrano object' do
19
+ ::Capistrano::Configuration.should_receive(:new).and_return(@cap)
20
+ create_cap
21
+ end
22
+
23
+ describe 'when verbose' do
24
+
25
+ before do
26
+ Sprinkle::OPTIONS[:verbose] = true
27
+ end
28
+
29
+ it 'should set verbose logging on the capistrano object' do
30
+ @cap = create_cap
31
+ @cap.config.logger.level.should == ::Capistrano::Logger::INFO
32
+ end
33
+
34
+ end
35
+
36
+ describe 'when not verbose' do
37
+
38
+ before do
39
+ Sprinkle::OPTIONS[:verbose] = false
40
+ end
41
+
42
+ it 'should set quiet logging on the capistrano object' do
43
+ @cap = create_cap
44
+ @cap.config.logger.level.should == ::Capistrano::Logger::IMPORTANT
45
+ end
46
+
47
+ end
48
+
49
+ describe 'with a block' do
50
+
51
+ before do
52
+ @actor = create_cap do
53
+ recipes 'cool gear' # default is deploy
54
+ end
55
+ end
56
+
57
+ it 'should evaluate the block against the actor instance' do
58
+ @actor.loaded_recipes.should include('cool gear')
59
+ end
60
+
61
+ end
62
+
63
+ describe 'without a block' do
64
+
65
+ it 'should automatically load the default capistrano configuration' do
66
+ @cap.should_receive(:load).with('deploy').and_return
67
+ end
68
+
69
+ after do
70
+ @actor = create_cap
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+
77
+ describe 'recipes' do
78
+
79
+ it 'should add the recipe location to an internal store' do
80
+ @cap = create_cap do
81
+ recipes 'deploy'
82
+ end
83
+ @cap.loaded_recipes.should == [ @recipes ]
84
+ end
85
+
86
+ it 'should load the given recipe' do
87
+ @cap.should_receive(:load).with(@recipes).and_return
88
+ create_cap
89
+ end
90
+
91
+ end
92
+
93
+ describe 'processing commands' do
94
+
95
+ before do
96
+ @commands = %w( op1 op2 )
97
+ @roles = %w( app )
98
+ @name = 'name'
99
+
100
+ @cap = create_cap do; recipes 'deploy'; end
101
+ @cap.stub!(:run).and_return
102
+ end
103
+
104
+ it 'should dynamically create a capistrano task containing the commands' do
105
+ @cap.config.should_receive(:task).and_return
106
+ end
107
+
108
+ it 'should invoke capistrano task after creation' do
109
+ @cap.should_receive(:run).with(@name).and_return
110
+ end
111
+
112
+ after do
113
+ @cap.process @name, @commands, @roles
114
+ end
115
+
116
+ end
117
+
118
+ describe 'generated task' do
119
+
120
+ before do
121
+ @commands = %w( op1 op2 )
122
+ @roles = %w( app )
123
+ @name = 'name'
124
+
125
+ @cap = create_cap do; recipes 'deploy'; end
126
+ @cap.config.stub!(:fetch).and_return(:sudo)
127
+ @cap.config.stub!(:invoke_command).and_return
128
+ end
129
+
130
+ it 'should use sudo to invoke commands when so configured' do
131
+ @cap.config.should_receive(:fetch).with(:run_method, :sudo).and_return(:sudo)
132
+ end
133
+
134
+ it 'should run the supplied commands' do
135
+ @cap.config.should_receive(:invoke_command).with('op1', :via => :sudo).ordered.and_return
136
+ @cap.config.should_receive(:invoke_command).with('op2', :via => :sudo).ordered.and_return
137
+ end
138
+
139
+ it 'should be applicable for the supplied roles' do
140
+ @cap.stub!(:run).and_return
141
+ @cap.config.should_receive(:task).with(:install_name, :roles => @roles).and_return
142
+ end
143
+
144
+ after do
145
+ @cap.process @name, @commands, @roles
146
+ end
147
+
148
+ end
149
+
150
+ end
@@ -0,0 +1,80 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Sprinkle::Deployment do
4
+ include Sprinkle::Deployment
5
+
6
+ def create_deployment(&block)
7
+ deployment do
8
+ delivery :capistrano, &block
9
+
10
+ source do
11
+ prefix '/usr/local'
12
+ end
13
+ end
14
+ end
15
+
16
+ describe 'when created' do
17
+
18
+ it 'should be invalid without a block descriptor' do
19
+ lambda { deployment }.should raise_error
20
+ end
21
+
22
+ it 'should be invalid without a delivery method' do
23
+ lambda { @deployment = deployment do; end }.should raise_error
24
+ end
25
+
26
+ it 'should optionally accept installer defaults' do
27
+ @deployment = create_deployment
28
+ @deployment.should respond_to(:source)
29
+ end
30
+
31
+ it 'should provide installer defaults as a proc when requested' do
32
+ @deployment = create_deployment
33
+ @deployment.defaults[:source].class.should == Proc
34
+ end
35
+
36
+ end
37
+
38
+ describe 'delivery specification' do
39
+
40
+ before do
41
+ @actor = mock(Sprinkle::Actors::Capistrano)
42
+ Sprinkle::Actors::Capistrano.stub!(:new).and_return(@actor)
43
+ end
44
+
45
+ it 'should automatically instantiate the delivery type' do
46
+ @deployment = create_deployment
47
+ @deployment.style.should == @actor
48
+ end
49
+
50
+ it 'should optionally accept a block to pass to the actor' do
51
+ lambda { @deployment = create_deployment }.should_not raise_error
52
+ end
53
+
54
+ describe 'with a block' do
55
+
56
+ it 'should pass the block to the actor for configuration' do
57
+ @deployment = create_deployment do; recipes 'deploy'; end
58
+ end
59
+
60
+ end
61
+ end
62
+
63
+ describe 'when processing policies' do
64
+
65
+ before do
66
+ @policy = mock(Policy, :process => true)
67
+ POLICIES = [ @policy ]
68
+ @deployment = create_deployment
69
+ end
70
+
71
+ it 'should apply all policies, passing itself as the deployment context' do
72
+ @policy.should_receive(:process).with(@deployment).and_return
73
+ end
74
+
75
+ after do
76
+ @deployment.process
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Array, 'task name conversions' do
4
+
5
+ it 'should be able to deliver a task name' do
6
+ ['build_essential'].to_task_name.should == 'build_essential'
7
+ end
8
+
9
+ it 'should join multiple elements together with a _ char' do
10
+ ['gdb', 'gcc', 'g++'].to_task_name.should == 'gdb_gcc_g++'
11
+ end
12
+
13
+ it 'should use the task name of the underlying array element' do
14
+ string = 'build-essential'
15
+ string.should_receive(:to_task_name).and_return('build_essential')
16
+ [string].to_task_name.should == 'build_essential'
17
+ end
18
+
19
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe String, 'task name conversions' do
4
+
5
+ it 'should be able to deliver a task name' do
6
+ 'build_essential'.to_task_name.should == 'build_essential'
7
+ end
8
+
9
+ it 'should convert all - chars to _ in the task name' do
10
+ 'build-essential'.to_task_name.should == 'build_essential'
11
+ end
12
+
13
+ it 'should convert multiple - chars to _ chars in the task name' do
14
+ 'build--essential'.to_task_name.should == 'build__essential'
15
+ end
16
+
17
+ it 'should lowercase the task name' do
18
+ 'BUILD-ESSENTIAL'.to_task_name.should == 'build_essential'
19
+ end
20
+
21
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe Sprinkle::Installers::Apt do
4
+
5
+ before do
6
+ @package = mock(Sprinkle::Package, :name => 'package')
7
+ end
8
+
9
+ def create_apt(debs, &block)
10
+ Sprinkle::Installers::Apt.new(@package, debs, &block)
11
+ end
12
+
13
+ describe 'when created' do
14
+
15
+ it 'should accept a single package to install' do
16
+ @installer = create_apt 'ruby'
17
+ @installer.packages.should == [ 'ruby' ]
18
+ end
19
+
20
+ it 'should accept an array of packages to install' do
21
+ @installer = create_apt %w( gcc gdb g++ )
22
+ @installer.packages.should == ['gcc', 'gdb', 'g++']
23
+ end
24
+
25
+ end
26
+
27
+ describe 'during installation' do
28
+
29
+ before do
30
+ @installer = create_apt 'ruby' do
31
+ pre :install, 'op1'
32
+ post :install, 'op2'
33
+ end
34
+ @install_commands = @installer.send :install_commands
35
+ end
36
+
37
+ it 'should invoke the apt installer for all specified packages' do
38
+ @install_commands.should =~ /apt-get -qyu install ruby/
39
+ end
40
+
41
+ it 'should specify a non interactive mode to the apt installer' do
42
+ @install_commands.should =~ /DEBIAN_FRONTEND=noninteractive/
43
+ end
44
+
45
+ it 'should automatically insert pre/post commands for the specified package' do
46
+ @installer.send(:install_sequence).should == [ 'op1', %(DEBCONF_TERSE='yes' DEBIAN_PRIORITY='critical' DEBIAN_FRONTEND=noninteractive apt-get -qyu install ruby), 'op2' ]
47
+ end
48
+
49
+ it 'should install a specific version if defined'
50
+
51
+ end
52
+
53
+ end