foreplay 0.9.13 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -1
- data/lib/foreplay/cli.rb +47 -45
- data/lib/foreplay/engine/defaults.rb +60 -0
- data/lib/foreplay/engine/logger.rb +50 -46
- data/lib/foreplay/engine/port.rb +65 -60
- data/lib/foreplay/engine/remote/check.rb +44 -38
- data/lib/foreplay/engine/remote/step.rb +34 -28
- data/lib/foreplay/engine/remote.rb +93 -86
- data/lib/foreplay/engine/role.rb +19 -15
- data/lib/foreplay/engine/secrets.rb +28 -24
- data/lib/foreplay/engine/server.rb +66 -55
- data/lib/foreplay/engine/step.rb +100 -96
- data/lib/foreplay/engine.rb +57 -106
- data/lib/foreplay/launcher.rb +9 -7
- data/lib/foreplay/setup.rb +26 -24
- data/lib/foreplay/version.rb +1 -1
- data/lib/foreplay.rb +14 -11
- data/spec/lib/foreplay/deploy_spec.rb +6 -2
- metadata +3 -4
- data/foreplay.rake +0 -28
- data/lib/foreplay/foreplay.rb +0 -9
@@ -1,92 +1,99 @@
|
|
1
1
|
require 'net/ssh'
|
2
2
|
require 'net/ssh/shell'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
4
|
+
require 'foreplay/engine/remote/check'
|
5
|
+
require 'foreplay/engine/remote/step'
|
6
|
+
|
7
|
+
module Foreplay
|
8
|
+
class Engine
|
9
|
+
class Remote
|
10
|
+
include Foreplay
|
11
|
+
attr_reader :server, :steps, :instructions
|
12
|
+
|
13
|
+
def initialize(s, st, i)
|
14
|
+
@server = s
|
15
|
+
@steps = st
|
16
|
+
@instructions = i
|
17
|
+
end
|
18
|
+
|
19
|
+
def deploy
|
20
|
+
output = ''
|
21
|
+
|
22
|
+
log "Connecting to #{host} on port #{port}", host: host
|
23
|
+
|
24
|
+
# SSH connection
|
25
|
+
session = start_session(host, user, options)
|
26
|
+
|
27
|
+
log "Successfully connected to #{host} on port #{port}", host: host
|
28
|
+
|
29
|
+
session.shell do |sh|
|
30
|
+
steps.each { |step| output += Foreplay::Engine::Remote::Step.new(host, sh, step, instructions).execute }
|
31
|
+
end
|
32
|
+
|
33
|
+
session.close
|
34
|
+
output
|
35
|
+
end
|
36
|
+
|
37
|
+
# Deployment check: just say what we would have done
|
38
|
+
def check
|
39
|
+
Foreplay::Engine::Remote::Check.new(host, steps, instructions).perform
|
40
|
+
end
|
41
|
+
|
42
|
+
def user
|
43
|
+
@user = instructions['user']
|
44
|
+
end
|
45
|
+
|
46
|
+
def host
|
47
|
+
@host ||= host_port[0]
|
48
|
+
end
|
49
|
+
|
50
|
+
def port
|
51
|
+
@port ||= (host_port[1] || 22)
|
52
|
+
end
|
53
|
+
|
54
|
+
def host_port
|
55
|
+
@host_port ||= server.split(':') # Parse host + port
|
56
|
+
end
|
57
|
+
|
58
|
+
def options
|
59
|
+
return @options if @options
|
60
|
+
|
61
|
+
@options = { verbose: :warn, port: port }
|
62
|
+
password = instructions['password']
|
63
|
+
|
64
|
+
if password.blank?
|
65
|
+
@options[:key_data] = [private_key]
|
66
|
+
else
|
67
|
+
@options[:password] = password
|
68
|
+
end
|
69
|
+
|
70
|
+
@options
|
71
|
+
end
|
72
|
+
|
73
|
+
def private_key
|
74
|
+
pk = instructions['private_key']
|
75
|
+
pk.blank? ? private_key_from_file : pk
|
76
|
+
end
|
77
|
+
|
78
|
+
def private_key_from_file
|
79
|
+
keyfile = instructions['keyfile']
|
80
|
+
keyfile.sub! '~', ENV['HOME'] || '/' unless keyfile.blank? # Remote shell won't expand this for us
|
81
|
+
|
82
|
+
terminate(
|
83
|
+
'No authentication methods supplied. '\
|
84
|
+
'You must supply a private key, key file or password in the configuration file'
|
85
|
+
) if keyfile.blank?
|
86
|
+
|
87
|
+
# Get the key from the key file
|
88
|
+
log "Using private key from #{keyfile}"
|
89
|
+
File.read keyfile
|
90
|
+
end
|
91
|
+
|
92
|
+
def start_session(host, user, options)
|
93
|
+
Net::SSH.start(host, user, options)
|
94
|
+
rescue SocketError => e
|
95
|
+
terminate "There was a problem starting an ssh session on #{host}:\n#{e.message}"
|
96
|
+
end
|
26
97
|
end
|
27
|
-
|
28
|
-
session.close
|
29
|
-
output
|
30
|
-
end
|
31
|
-
|
32
|
-
# Deployment check: just say what we would have done
|
33
|
-
def check
|
34
|
-
Foreplay::Engine::Remote::Check.new(host, steps, instructions).perform
|
35
|
-
end
|
36
|
-
|
37
|
-
def user
|
38
|
-
@user = instructions['user']
|
39
|
-
end
|
40
|
-
|
41
|
-
def host
|
42
|
-
@host ||= host_port[0]
|
43
|
-
end
|
44
|
-
|
45
|
-
def port
|
46
|
-
@port ||= (host_port[1] || 22)
|
47
|
-
end
|
48
|
-
|
49
|
-
def host_port
|
50
|
-
@host_port ||= server.split(':') # Parse host + port
|
51
|
-
end
|
52
|
-
|
53
|
-
def options
|
54
|
-
return @options if @options
|
55
|
-
|
56
|
-
@options = { verbose: :warn, port: port }
|
57
|
-
password = instructions['password']
|
58
|
-
|
59
|
-
if password.blank?
|
60
|
-
@options[:key_data] = [private_key]
|
61
|
-
else
|
62
|
-
@options[:password] = password
|
63
|
-
end
|
64
|
-
|
65
|
-
@options
|
66
|
-
end
|
67
|
-
|
68
|
-
def private_key
|
69
|
-
pk = instructions['private_key']
|
70
|
-
pk.blank? ? private_key_from_file : pk
|
71
|
-
end
|
72
|
-
|
73
|
-
def private_key_from_file
|
74
|
-
keyfile = instructions['keyfile']
|
75
|
-
keyfile.sub! '~', ENV['HOME'] || '/' unless keyfile.blank? # Remote shell won't expand this for us
|
76
|
-
|
77
|
-
terminate(
|
78
|
-
'No authentication methods supplied. '\
|
79
|
-
'You must supply a private key, key file or password in the configuration file'
|
80
|
-
) if keyfile.blank?
|
81
|
-
|
82
|
-
# Get the key from the key file
|
83
|
-
log "Using private key from #{keyfile}"
|
84
|
-
File.read keyfile
|
85
|
-
end
|
86
|
-
|
87
|
-
def start_session(host, user, options)
|
88
|
-
Net::SSH.start(host, user, options)
|
89
|
-
rescue SocketError => e
|
90
|
-
terminate "There was a problem starting an ssh session on #{host}:\n#{e.message}"
|
91
98
|
end
|
92
99
|
end
|
data/lib/foreplay/engine/role.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
module Foreplay
|
2
|
+
class Engine
|
3
|
+
class Role
|
4
|
+
attr_reader :environment, :mode, :instructions, :servers
|
5
|
+
def initialize(e, m, i)
|
6
|
+
@environment = e
|
7
|
+
@mode = m
|
8
|
+
@instructions = i
|
9
|
+
@servers = @instructions['servers']
|
8
10
|
|
9
|
-
|
11
|
+
preposition = mode == :deploy ? 'to' : 'for'
|
10
12
|
|
11
|
-
|
13
|
+
return if @servers.length == 1
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
15
|
+
puts "#{mode.capitalize}ing #{instructions['name'].yellow} #{preposition} #{@servers.join(', ').yellow} "\
|
16
|
+
"for the #{instructions['role'].dup.yellow} role in the #{environment.dup.yellow} environment..."
|
17
|
+
end
|
16
18
|
|
17
|
-
|
18
|
-
|
19
|
-
|
19
|
+
def threads
|
20
|
+
servers.map do |server|
|
21
|
+
Thread.new { Foreplay::Engine::Server.new(environment, mode, instructions, server).execute }
|
22
|
+
end
|
23
|
+
end
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
@@ -1,34 +1,38 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Foreplay
|
2
|
+
class Engine
|
3
|
+
class Secrets
|
4
|
+
attr_reader :environment, :secret_locations
|
3
5
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
6
|
+
def initialize(e, sl)
|
7
|
+
@environment = e
|
8
|
+
@secret_locations = sl
|
9
|
+
end
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
+
def fetch
|
12
|
+
return unless secret_locations
|
11
13
|
|
12
|
-
|
14
|
+
secrets = {}
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
secret_locations.each do |secret_location|
|
17
|
+
secrets.merge! fetch_from(secret_location) || {}
|
18
|
+
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
+
secrets
|
21
|
+
end
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
+
def fetch_from(secret_location)
|
24
|
+
url = secret_location['url'] || return
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
headers = secret_location['headers']
|
27
|
+
header_string = headers.map { |k, v| " -H \"#{k}: #{v}\"" }.join if headers.is_a? Hash
|
28
|
+
command = "curl -k -L#{header_string} #{url}".fake_erb
|
29
|
+
secrets_all = YAML.load(`#{command}`)
|
30
|
+
secrets = secrets_all[environment]
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
secrets if secrets.is_a? Hash
|
33
|
+
rescue Psych::SyntaxError
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
end
|
33
37
|
end
|
34
38
|
end
|
@@ -1,68 +1,79 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
module Foreplay
|
2
|
+
class Engine
|
3
|
+
class Server
|
4
|
+
include Foreplay::Engine::Port
|
5
|
+
attr_reader :environment, :mode, :instructions, :server
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
def initialize(e, m, i, s)
|
8
|
+
@environment = e
|
9
|
+
@mode = m
|
10
|
+
@instructions = i
|
11
|
+
@server = s
|
12
|
+
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Foreplay::Engine::Remote.new(server, steps, instructions).__send__ mode
|
17
|
-
end
|
14
|
+
def execute
|
15
|
+
execute_announce
|
16
|
+
foreman
|
17
|
+
env
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
puts "#{mode.capitalize}ing #{name.yellow} #{preposition} #{host.yellow} "\
|
22
|
-
"for the #{role.dup.yellow} role in the #{environment.dup.yellow} environment"
|
23
|
-
end
|
19
|
+
Foreplay::Engine::Remote.new(server, steps, instructions).__send__ mode
|
20
|
+
end
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
'log' => "$HOME/#{path}/#{current_port}/log"
|
31
|
-
)
|
32
|
-
end
|
22
|
+
def execute_announce
|
23
|
+
preposition = mode == :deploy ? 'to' : 'for'
|
24
|
+
puts "#{mode.capitalize}ing #{name.yellow} #{preposition} #{host.yellow} "\
|
25
|
+
"for the #{role.dup.yellow} role in the #{environment.dup.yellow} environment"
|
26
|
+
end
|
33
27
|
|
34
|
-
|
35
|
-
|
36
|
-
'HOME' => '$HOME',
|
37
|
-
'SHELL' => '$SHELL',
|
38
|
-
'PATH' => '$PATH:`which bundle`'
|
39
|
-
)
|
40
|
-
end
|
28
|
+
def foreman
|
29
|
+
instructions['foreman'] = {} unless instructions.key? 'foreman'
|
41
30
|
|
42
|
-
|
43
|
-
|
44
|
-
|
31
|
+
instructions['foreman'].merge!(
|
32
|
+
'app' => current_service,
|
33
|
+
'port' => current_port,
|
34
|
+
'user' => user,
|
35
|
+
'log' => "$HOME/#{path}/#{current_port}/log"
|
36
|
+
)
|
37
|
+
end
|
45
38
|
|
46
|
-
|
47
|
-
|
48
|
-
end
|
39
|
+
def env
|
40
|
+
instructions['env'] = {} unless instructions.key? 'env'
|
49
41
|
|
50
|
-
|
51
|
-
|
42
|
+
instructions['env'].merge!(
|
43
|
+
'HOME' => '$HOME',
|
44
|
+
'SHELL' => '$SHELL',
|
45
|
+
'PATH' => '$PATH:`which bundle`',
|
46
|
+
'GEM_HOME' => '$HOME/.rvm/gems/`rvm tools identifier`',
|
47
|
+
'RAILS_ENV' => environment
|
48
|
+
)
|
49
|
+
end
|
52
50
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
51
|
+
def role
|
52
|
+
@role ||= instructions['role']
|
53
|
+
end
|
54
|
+
|
55
|
+
def user
|
56
|
+
@user ||= instructions['user']
|
57
|
+
end
|
58
|
+
|
59
|
+
def path
|
60
|
+
return @path if @path
|
61
|
+
|
62
|
+
@path = instructions['path']
|
63
|
+
@path.gsub! '%u', user
|
64
|
+
@path.gsub! '%a', name
|
65
|
+
@path
|
66
|
+
end
|
58
67
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
68
|
+
def steps
|
69
|
+
@steps ||= YAML.load(
|
70
|
+
ERB.new(
|
71
|
+
File.read(
|
72
|
+
"#{File.dirname(__FILE__)}/steps.yml"
|
73
|
+
)
|
74
|
+
).result(binding)
|
64
75
|
)
|
65
|
-
|
66
|
-
|
76
|
+
end
|
77
|
+
end
|
67
78
|
end
|
68
79
|
end
|
data/lib/foreplay/engine/step.rb
CHANGED
@@ -1,117 +1,121 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def commands
|
12
|
-
return @commands if @commands
|
13
|
-
|
14
|
-
# Each step can be (1) a command or (2) a series of values to add to a file
|
15
|
-
if step.key?('key')
|
16
|
-
if instructions.key?(step['key'])
|
17
|
-
build_commands
|
18
|
-
else
|
19
|
-
@commands = []
|
1
|
+
module Foreplay
|
2
|
+
class Engine
|
3
|
+
class Step
|
4
|
+
include Foreplay
|
5
|
+
attr_reader :host, :step, :instructions
|
6
|
+
|
7
|
+
def initialize(h, s, i)
|
8
|
+
@host = h
|
9
|
+
@step = s
|
10
|
+
@instructions = i
|
20
11
|
end
|
21
|
-
else
|
22
|
-
# ...or just execute the command specified
|
23
|
-
@commands = [command]
|
24
|
-
end
|
25
|
-
|
26
|
-
@commands
|
27
|
-
end
|
28
12
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
13
|
+
def commands
|
14
|
+
return @commands if @commands
|
15
|
+
|
16
|
+
# Each step can be (1) a command or (2) a series of values to add to a file
|
17
|
+
if step.key?('key')
|
18
|
+
if instructions.key?(step['key'])
|
19
|
+
build_commands
|
20
|
+
else
|
21
|
+
@commands = []
|
22
|
+
end
|
23
|
+
else
|
24
|
+
# ...or just execute the command specified
|
25
|
+
@commands = [command]
|
26
|
+
end
|
27
|
+
|
28
|
+
@commands
|
29
|
+
end
|
38
30
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
31
|
+
def build_commands
|
32
|
+
step['silent'] = true
|
33
|
+
|
34
|
+
if header?
|
35
|
+
@commands = ["echo \"#{header}\" > #{filename}"]
|
36
|
+
redirect
|
37
|
+
else
|
38
|
+
@commands = []
|
39
|
+
end
|
40
|
+
|
41
|
+
if instructions[key].is_a? Hash
|
42
|
+
build_commands_from_hash
|
43
|
+
else
|
44
|
+
build_commands_from_string
|
45
|
+
end
|
46
|
+
end
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
def build_commands_from_hash
|
49
|
+
instructions[key].each do |k, v|
|
50
|
+
@commands << "echo \"#{before}#{k}#{delimiter}#{v}#{after}\" #{redirect} #{filename}"
|
51
|
+
end
|
52
|
+
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
def build_commands_from_string
|
55
|
+
@commands << "echo \"#{before}#{delimiter}#{instructions[key]}#{after}\" #{redirect} #{filename}"
|
56
|
+
end
|
55
57
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
58
|
+
def redirect
|
59
|
+
if @redirect
|
60
|
+
'>>'
|
61
|
+
else
|
62
|
+
@redirect = true
|
63
|
+
'>'
|
64
|
+
end
|
65
|
+
end
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
-
|
67
|
+
def command
|
68
|
+
@command ||= step['command']
|
69
|
+
end
|
68
70
|
|
69
|
-
|
70
|
-
|
71
|
-
|
71
|
+
def filename
|
72
|
+
@filename ||= "#{path}#{prefix}#{key}#{suffix}"
|
73
|
+
end
|
72
74
|
|
73
|
-
|
74
|
-
|
75
|
-
|
75
|
+
def key
|
76
|
+
@key ||= step['key']
|
77
|
+
end
|
76
78
|
|
77
|
-
|
78
|
-
|
79
|
-
|
79
|
+
def prefix
|
80
|
+
@prefix ||= step['prefix'] || ''
|
81
|
+
end
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
-
|
83
|
+
def suffix
|
84
|
+
@suffix ||= step['suffix'] || ''
|
85
|
+
end
|
84
86
|
|
85
|
-
|
86
|
-
|
87
|
-
|
87
|
+
def path
|
88
|
+
@path ||= step['path'] || ''
|
89
|
+
end
|
88
90
|
|
89
|
-
|
90
|
-
|
91
|
-
|
91
|
+
def before
|
92
|
+
@before ||= step['before'] || ''
|
93
|
+
end
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
-
|
95
|
+
def delimiter
|
96
|
+
@delimiter ||= step['delimiter'] || ''
|
97
|
+
end
|
96
98
|
|
97
|
-
|
98
|
-
|
99
|
-
|
99
|
+
def after
|
100
|
+
@after ||= step['after'] || ''
|
101
|
+
end
|
100
102
|
|
101
|
-
|
102
|
-
|
103
|
-
|
103
|
+
def header
|
104
|
+
@header ||= step['header']
|
105
|
+
end
|
104
106
|
|
105
|
-
|
106
|
-
|
107
|
-
|
107
|
+
def header?
|
108
|
+
header.present?
|
109
|
+
end
|
108
110
|
|
109
|
-
|
110
|
-
|
111
|
-
|
111
|
+
def silent
|
112
|
+
@silent ||= step['silent']
|
113
|
+
end
|
112
114
|
|
113
|
-
|
114
|
-
|
115
|
-
|
115
|
+
def announce
|
116
|
+
log "#{(step['commentary'] || command).yellow}", host: host, silent: silent
|
117
|
+
log command.cyan, host: host, silent: silent if instructions['verbose'] && step['commentary'] && command
|
118
|
+
end
|
119
|
+
end
|
116
120
|
end
|
117
121
|
end
|