foreplay 0.7.6 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,138 @@
1
+ require 'yaml'
2
+ require 'string'
3
+
4
+ class Foreplay::Engine
5
+ include Foreplay
6
+ attr_reader :environment, :filters, :mode
7
+
8
+ DEFAULTS_KEY = 'defaults'
9
+ CONFIG_FILE = "#{Dir.getwd}/config/foreplay.yml"
10
+
11
+ def initialize(e, f)
12
+ @environment = e
13
+ @filters = f
14
+ end
15
+
16
+ def deploy
17
+ @mode = :deploy
18
+ execute
19
+ end
20
+
21
+ def check
22
+ @mode = :check
23
+ execute
24
+ end
25
+
26
+ def execute
27
+ # Explain what we're going to do
28
+ puts "#{mode.capitalize}ing #{environment.dup.yellow} environment, "\
29
+ "#{explanatory_text(filters, 'role')}, #{explanatory_text(filters, 'server')}"
30
+
31
+ threads = []
32
+
33
+ roles.each do |role, additional_instructions|
34
+ next if role == DEFAULTS_KEY # 'defaults' is not a role
35
+ next if filters.key?('role') && filters['role'] != role
36
+
37
+ threads.concat Foreplay::Engine::Role.new(
38
+ environment,
39
+ mode,
40
+ build_instructions(role, additional_instructions)
41
+ ).threads
42
+ end
43
+
44
+ threads.each(&:join)
45
+
46
+ puts mode == :deploy ? 'Finished deployment' : 'Deployment configuration check was successful'
47
+ end
48
+
49
+ # Returns a new hash with +hash+ and +other_hash+ merged recursively, including arrays.
50
+ #
51
+ # h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
52
+ # h2 = { x: { y: [7,8,9] }, z: 'xyz' }
53
+ # h1.supermerge(h2)
54
+ # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
55
+ def supermerge(hash, other_hash)
56
+ fail 'supermerge only works if you pass two hashes. '\
57
+ "You passed a #{hash.class} and a #{other_hash.class}." unless hash.is_a?(Hash) && other_hash.is_a?(Hash)
58
+
59
+ new_hash = hash.deep_dup.with_indifferent_access
60
+
61
+ other_hash.each_pair do |k, v|
62
+ tv = new_hash[k]
63
+
64
+ if tv.is_a?(Hash) && v.is_a?(Hash)
65
+ new_hash[k] = supermerge(tv, v)
66
+ elsif tv.is_a?(Array) || v.is_a?(Array)
67
+ new_hash[k] = Array.wrap(tv) + Array.wrap(v)
68
+ else
69
+ new_hash[k] = v
70
+ end
71
+ end
72
+
73
+ new_hash
74
+ end
75
+
76
+ private
77
+
78
+ def explanatory_text(hsh, key)
79
+ hsh.key?(key) ? "#{hsh[key].dup.yellow} #{key}" : "all #{key.pluralize}"
80
+ end
81
+
82
+ def build_instructions(role, additional_instructions)
83
+ instructions = supermerge(defaults, additional_instructions).symbolize_keys
84
+ instructions[:role] = role
85
+ required_keys = [:name, :environment, :role, :servers, :path, :repository]
86
+
87
+ required_keys.each do |key|
88
+ next if instructions.key? key
89
+ terminate("Required key #{key} not found in instructions for #{environment} environment.\nCheck #{CONFIG_FILE}")
90
+ end
91
+
92
+ # Apply server filter
93
+ instructions[:servers] &= server_filter if server_filter
94
+ instructions
95
+ end
96
+
97
+ def server_filter
98
+ @server_filter ||= filters['server'].split(',') if filters.key?('server')
99
+ end
100
+
101
+ def defaults
102
+ return @defaults if @defaults
103
+
104
+ # Establish defaults
105
+ # First the default defaults
106
+ @defaults = {
107
+ name: File.basename(Dir.getwd),
108
+ environment: environment,
109
+ env: { 'RAILS_ENV' => environment },
110
+ port: 50_000
111
+ }
112
+
113
+ @defaults = supermerge(@defaults, roles_all[DEFAULTS_KEY]) if roles_all.key? DEFAULTS_KEY
114
+ @defaults = supermerge(@defaults, roles[DEFAULTS_KEY]) if roles.key? DEFAULTS_KEY
115
+ end
116
+
117
+ def roles
118
+ @roles ||= roles_all[environment]
119
+ end
120
+
121
+ def roles_all
122
+ return @roles_all if @roles_all
123
+
124
+ @roles_all = YAML.load(File.read(CONFIG_FILE))
125
+
126
+ # This environment
127
+ unless @roles_all.key? environment
128
+ terminate("No deployment configuration defined for #{environment} environment.\nCheck #{CONFIG_FILE}")
129
+ end
130
+
131
+ @roles_all
132
+ rescue Errno::ENOENT
133
+ terminate "Can't find configuration file #{CONFIG_FILE}.\nPlease run foreplay setup or create the file manually."
134
+ rescue Psych::SyntaxError
135
+ terminate "I don't understand the configuration file #{CONFIG_FILE}.\n"\
136
+ 'Please run foreplay setup or edit the file manually.'
137
+ end
138
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_support/core_ext/object'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Foreplay
5
+ INDENT = "\t"
6
+
7
+ def terminate(message)
8
+ fail message
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ require 'thor/group'
2
+
3
+ class Foreplay::Launcher < Thor::Group
4
+ include Thor::Actions
5
+
6
+ argument :mode, type: :string, required: true
7
+ argument :environment, type: :string, required: true
8
+ argument :filters, type: :hash, required: false
9
+
10
+ def parse
11
+ Foreplay::Engine.new(environment, filters).__send__ mode
12
+ end
13
+ end
@@ -1,33 +1,31 @@
1
1
  require 'thor/group'
2
2
 
3
- module Foreplay
4
- class Setup < Thor::Group
5
- include Thor::Actions
3
+ class Foreplay::Setup < Thor::Group
4
+ include Thor::Actions
6
5
 
7
- class_option :name, aliases: '-n', default: File.basename(Dir.getwd)
8
- class_option :repository, aliases: '-r'
9
- class_option :user, aliases: '-u'
10
- class_option :password
11
- class_option :keyfile
12
- class_option :private_key, aliases: '-k'
13
- class_option :port, aliases: '-p', default: 50_000
14
- class_option :path, aliases: '-f'
15
- class_option :servers, aliases: '-s', type: :array
16
- class_option :db_adapter, aliases: '-a', default: 'postgresql'
17
- class_option :db_encoding, aliases: '-e', default: 'utf8'
18
- class_option :db_pool, default: 5
19
- class_option :db_name, aliases: '-d'
20
- class_option :db_host, aliases: '-h'
21
- class_option :db_user
22
- class_option :db_password
23
- class_option :resque_redis
6
+ class_option :name, aliases: '-n', default: File.basename(Dir.getwd)
7
+ class_option :repository, aliases: '-r'
8
+ class_option :user, aliases: '-u'
9
+ class_option :password
10
+ class_option :keyfile
11
+ class_option :private_key, aliases: '-k'
12
+ class_option :port, aliases: '-p', default: 50_000
13
+ class_option :path, aliases: '-f'
14
+ class_option :servers, aliases: '-s', type: :array
15
+ class_option :db_adapter, aliases: '-a', default: 'postgresql'
16
+ class_option :db_encoding, aliases: '-e', default: 'utf8'
17
+ class_option :db_pool, default: 5
18
+ class_option :db_name, aliases: '-d'
19
+ class_option :db_host, aliases: '-h'
20
+ class_option :db_user
21
+ class_option :db_password
22
+ class_option :resque_redis
24
23
 
25
- def self.source_root
26
- File.dirname(__FILE__)
27
- end
24
+ def self.source_root
25
+ File.dirname(__FILE__)
26
+ end
28
27
 
29
- def config
30
- template('setup/foreplay.yml', 'config/foreplay.yml')
31
- end
28
+ def config
29
+ template('setup/foreplay.yml', 'config/foreplay.yml')
32
30
  end
33
31
  end
@@ -1,3 +1,3 @@
1
1
  module Foreplay
2
- VERSION = '0.7.6'
2
+ VERSION = '0.8.0'
3
3
  end
data/lib/foreplay.rb CHANGED
@@ -1,4 +1,8 @@
1
1
  require 'foreplay/version'
2
- require 'foreplay/setup'
3
- require 'foreplay/deploy'
4
- require 'foreplay/utility'
2
+ require 'foreplay/foreplay'
3
+ require 'foreplay/launcher'
4
+ require 'foreplay/engine'
5
+ require 'foreplay/engine/remote'
6
+ require 'foreplay/engine/role'
7
+ require 'foreplay/engine/server'
8
+ require 'foreplay/engine/step'
data/lib/string.rb ADDED
@@ -0,0 +1,17 @@
1
+ # Some useful additions to the String class
2
+ class String
3
+ colors = %w(black red green yellow blue magenta cyan white)
4
+
5
+ colors.each_with_index do |fg_color, i|
6
+ fg = 30 + i
7
+ define_method(fg_color) { ansi_attributes(fg) }
8
+
9
+ colors.each_with_index do |bg_color, j|
10
+ define_method("#{fg_color}_on_#{bg_color}") { ansi_attributes(fg, 40 + j) }
11
+ end
12
+ end
13
+
14
+ def ansi_attributes(*args)
15
+ "\e[#{args.join(';')}m#{self}\e[0m"
16
+ end
17
+ end
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
- require 'foreplay'
3
2
  require 'net/ssh/shell'
3
+ require 'foreplay'
4
4
 
5
- describe Foreplay::Deploy do
5
+ describe Foreplay::Launcher do
6
6
  let(:session) { double(Net::SSH::Connection::Session) }
7
7
  let(:shell) { double(Net::SSH::Shell) }
8
8
  let(:process) { double(Net::SSH::Shell::Process) }
@@ -10,7 +10,13 @@ describe Foreplay::Deploy do
10
10
  before :each do
11
11
  # Setup foreplay
12
12
  `rm -f config/foreplay.yml`
13
- `foreplay setup -r git@github.com:Xenapto/foreplay.git -s web1.example.com web2.example.com -f apps/%a -u fred --password trollope`
13
+ command = 'foreplay setup '\
14
+ '-r git@github.com:Xenapto/foreplay.git '\
15
+ '-s web1.example.com web2.example.com '\
16
+ '-f apps/%a '\
17
+ '-u fred '\
18
+ '--password trollope'
19
+ `#{command}`
14
20
 
15
21
  # Stub all the things
16
22
  allow(Net::SSH).to receive(:start).and_return(session)
@@ -24,37 +30,68 @@ describe Foreplay::Deploy do
24
30
 
25
31
  it "complains on check if there's no config file" do
26
32
  `rm -f config/foreplay.yml`
27
- expect { Foreplay::Deploy.start([:check, 'production', '']) }
33
+ expect { Foreplay::Launcher.start([:check, 'production', '']) }
28
34
  .to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
29
35
  end
30
36
 
31
37
  it "complains on deploy if there's no config file" do
32
38
  `rm -f config/foreplay.yml`
33
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }
39
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
34
40
  .to raise_error(RuntimeError, /.*Please run foreplay setup or create the file manually.*/)
35
41
  end
36
42
 
43
+ it 'complains on check if the config file is not valid YAML' do
44
+ `echo %*:*: > config/foreplay.yml`
45
+ expect { Foreplay::Launcher.start([:check, 'production', '']) }
46
+ .to raise_error(RuntimeError, /.*Please run foreplay setup or edit the file manually.*/)
47
+ end
48
+
49
+ it 'complains on deploy if the config file is not valid YAML' do
50
+ `echo %*:*: > config/foreplay.yml`
51
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
52
+ .to raise_error(RuntimeError, /.*Please run foreplay setup or edit the file manually.*/)
53
+ end
54
+
37
55
  it 'complains if there are no authentication methods defined in the config file' do
56
+ command = 'foreplay setup '\
57
+ '-r git@github.com:Xenapto/foreplay.git '\
58
+ '-s web.example.com '\
59
+ '-f apps/%a '\
60
+ '-u fred '\
61
+ '--password ""'
62
+
63
+ match = 'No authentication methods supplied. '\
64
+ 'You must supply a private key, key file or password in the configuration file.'
65
+
38
66
  `rm -f config/foreplay.yml`
39
- `foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --password ""`
40
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }
67
+ `#{command}`
68
+
69
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
41
70
  .to raise_error(
42
71
  RuntimeError,
43
- /.*No authentication methods supplied. You must supply a private key, key file or password in the configuration file.*/
72
+ /.*#{Regexp.quote(match)}*/
44
73
  )
45
74
  end
46
75
 
47
76
  it "complains if the private keyfile defined in the config file doesn't exist" do
77
+ command = 'foreplay setup '\
78
+ '-r git@github.com:Xenapto/foreplay.git '\
79
+ '-s web.example.com '\
80
+ '-f apps/%a '\
81
+ '-u fred '\
82
+ '--keyfile "/home/fred/no-such-file"'
83
+
48
84
  `rm -f config/foreplay.yml`
49
- `foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --keyfile "/home/fred/no-such-file"`
50
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }
85
+ `#{command}`
86
+
87
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
51
88
  .to raise_error(Errno::ENOENT, %r{.*No such file or directory @ rb_sysopen - /home/fred/no-such-file.*})
52
89
  end
53
90
 
54
91
  it 'complains if a mandatory key is missing from the config file' do
55
92
  `sed -i 's/path:/pxth:/' config/foreplay.yml`
56
93
 
57
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }
94
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
58
95
  .to raise_error(
59
96
  RuntimeError,
60
97
  /.*Required key path not found in instructions for production environment.*/
@@ -62,7 +99,7 @@ describe Foreplay::Deploy do
62
99
  end
63
100
 
64
101
  it 'complains if we try to deploy an environment that isn\'t defined' do
65
- expect { Foreplay::Deploy.start([:deploy, 'unknown', '']) }
102
+ expect { Foreplay::Launcher.start([:deploy, 'unknown', '']) }
66
103
  .to raise_error(
67
104
  RuntimeError,
68
105
  /.*No deployment configuration defined for unknown environment.*/
@@ -71,18 +108,18 @@ describe Foreplay::Deploy do
71
108
 
72
109
  it 'terminates if a remote process exits with a non-zero status' do
73
110
  allow(process).to receive(:exit_status).and_return(1)
74
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*output message.*/)
111
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }.to raise_error(RuntimeError, /.*output message.*/)
75
112
  end
76
113
 
77
114
  it "terminates if a connection can't be established with the remote server" do
78
115
  allow(Net::SSH).to receive(:start).and_call_original
79
- expect { Foreplay::Deploy.start([:deploy, 'production', '']) }
116
+ expect { Foreplay::Launcher.start([:deploy, 'production', '']) }
80
117
  .to raise_error(RuntimeError, /.*There was a problem starting an ssh session on web1.example.com.*/)
81
118
  end
82
119
 
83
120
  it 'checks the config' do
84
121
  expect($stdout).to receive(:puts).at_least(:once)
85
- Foreplay::Deploy.start [:check, 'production', '']
122
+ Foreplay::Launcher.start [:check, 'production', '']
86
123
  end
87
124
 
88
125
  it 'deploys to the environment' do
@@ -93,7 +130,9 @@ describe Foreplay::Deploy do
93
130
  .and_return(session)
94
131
 
95
132
  [
96
- 'mkdir -p apps/foreplay && cd apps/foreplay && rm -rf 50000 && git clone -b master git@github.com:Xenapto/foreplay.git 50000',
133
+ "echo Foreplay version #{Foreplay::VERSION}",
134
+ 'mkdir -p apps/foreplay && cd apps/foreplay && rm -rf 50000 && '\
135
+ 'git clone -b master git@github.com:Xenapto/foreplay.git 50000',
97
136
  'rvm rvmrc trust 50000',
98
137
  'rvm rvmrc warning ignore 50000',
99
138
  'cd 50000 && mkdir -p tmp doc log config',
@@ -113,11 +152,13 @@ describe Foreplay::Deploy do
113
152
  'echo " password: TODO Put here the database user\'s password" >> config/database.yml',
114
153
  'if [ -d ../cache/vendor/bundle/bundle ] ; then rm -rf ../cache/vendor/bundle/bundle'\
115
154
  ' ; else echo No evidence of legacy copy bug ; fi',
116
- 'if [ -d ../cache/vendor/bundle ] ; then rsync -aW --no-compress --delete --info=STATS3 ../cache/vendor/bundle/ vendor/bundle'\
155
+ 'if [ -d ../cache/vendor/bundle ] ; then '\
156
+ 'rsync -aW --no-compress --delete --info=STATS1 ../cache/vendor/bundle/ vendor/bundle'\
117
157
  ' ; else echo No bundle to restore ; fi',
118
158
  'sudo ln -f `which bundle` /usr/bin/bundle || echo Using default version of bundle',
119
159
  'bundle install --deployment --clean --jobs 2 --without development test',
120
- 'mkdir -p ../cache/vendor && rsync -aW --no-compress --delete --info=STATS1 vendor/bundle/ ../cache/vendor/bundle',
160
+ 'mkdir -p ../cache/vendor && '\
161
+ 'rsync -aW --no-compress --delete --info=STATS1 vendor/bundle/ ../cache/vendor/bundle',
121
162
  'if [ -f public/assets/manifest.yml ] ; then echo "Not precompiling assets"'\
122
163
  ' ; else RAILS_ENV=production bundle exec foreman run rake assets:precompile ; fi',
123
164
  'sudo bundle exec foreman export upstart /etc/init',
@@ -135,24 +176,38 @@ describe Foreplay::Deploy do
135
176
  expect(shell).to receive(:execute).with(command).and_return(process)
136
177
  end
137
178
 
138
- Foreplay::Deploy.start [:deploy, 'production', '']
179
+ Foreplay::Launcher.start [:deploy, 'production', '']
139
180
  end
140
181
 
141
182
  it "uses another port if there's already an installed instance" do
142
183
  allow(process).to receive(:on_output).and_yield(process, "50000\n")
143
184
  expect(shell).to receive(:execute).with('echo 51000 > $HOME/.foreplay/foreplay/current_port').and_return(process)
144
- Foreplay::Deploy.start [:deploy, 'production', '']
185
+ Foreplay::Launcher.start [:deploy, 'production', '']
145
186
  end
146
187
 
147
188
  it 'uses the private key provided in the config file' do
189
+ command = 'foreplay setup '\
190
+ '-r git@github.com:Xenapto/foreplay.git '\
191
+ '-s web.example.com '\
192
+ '-f apps/%a '\
193
+ '-u fred '\
194
+ '-k "top secret private key"'
195
+
148
196
  `rm -f config/foreplay.yml`
149
- `foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred -k "top secret private key"`
150
- Foreplay::Deploy.start([:deploy, 'production', ''])
197
+ `#{command}`
198
+ Foreplay::Launcher.start([:deploy, 'production', ''])
151
199
  end
152
200
 
153
201
  it 'adds Redis details for Resque' do
202
+ command = 'foreplay setup '\
203
+ '-r git@github.com:Xenapto/foreplay.git '\
204
+ '-s web.example.com '\
205
+ '-f apps/%a '\
206
+ '-u fred '\
207
+ '--resque-redis "redis://localhost:6379"'
208
+
154
209
  `rm -f config/foreplay.yml`
155
- `foreplay setup -r git@github.com:Xenapto/foreplay.git -s web.example.com -f apps/%a -u fred --resque-redis "redis://localhost:6379"`
156
- Foreplay::Deploy.start([:deploy, 'production', ''])
210
+ `#{command}`
211
+ Foreplay::Launcher.start([:deploy, 'production', ''])
157
212
  end
158
213
  end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'foreplay'
3
+
4
+ describe Foreplay::Engine do
5
+ context '#supermerge' do
6
+ let(:engine) { Foreplay::Engine.new('b', 'b') }
7
+
8
+ it 'should complain unless two hashes are passed to it' do
9
+ expect { engine.supermerge('x', 'y') }.to raise_error(RuntimeError)
10
+ end
11
+
12
+ it 'should merge two simple hashes' do
13
+ expect(engine.supermerge({ a: 'x' }, b: 'y')).to eq('a' => 'x', 'b' => 'y')
14
+ end
15
+
16
+ it 'should merge two hashes both with arrays at the same key' do
17
+ expect(engine.supermerge({ a: ['x'] }, a: ['y'])).to eq('a' => %w(x y))
18
+ end
19
+
20
+ it 'should merge an array and a value at the same key' do
21
+ expect(engine.supermerge({ a: 'x' }, a: ['y'])).to eq('a' => %w(x y))
22
+ end
23
+
24
+ it 'should replace a value at the same key' do
25
+ expect(engine.supermerge({ a: 'x' }, a: 'y')).to eq('a' => 'y')
26
+ end
27
+
28
+ it 'should merge two subhashes at the same key' do
29
+ expect(engine.supermerge({ a: { b: 'x' } }, a: { c: 'y' })).to eq('a' => { 'b' => 'x', 'c' => 'y' })
30
+ end
31
+ end
32
+ end
@@ -1,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require 'foreplay'
2
+ require 'foreplay/setup'
3
3
 
4
4
  describe Foreplay::Setup do
5
5
  before :each do