foreplay 0.7.6 → 0.8.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.
@@ -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