sprinkle 0.1.4

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.
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