mau 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +5 -0
- data/bin/mau +10 -0
- data/lib/app_update.rb +110 -0
- data/lib/application.rb +176 -0
- data/lib/configuration.rb +35 -0
- data/lib/delegation.rb +36 -0
- data/lib/runner.rb +63 -0
- metadata +71 -0
data/README
ADDED
data/bin/mau
ADDED
data/lib/app_update.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
require 'logger'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
require 'configuration'
|
7
|
+
require 'application'
|
8
|
+
require 'delegation'
|
9
|
+
require 'runner'
|
10
|
+
|
11
|
+
# Main class of the app update tool.
|
12
|
+
class AppUpdate
|
13
|
+
extend Delegation
|
14
|
+
|
15
|
+
APP_CONFIG = '.applications'
|
16
|
+
|
17
|
+
attr_reader :opts
|
18
|
+
attr_reader :runner
|
19
|
+
attr_reader :logger
|
20
|
+
attr_reader :config
|
21
|
+
attr_reader :extra_args
|
22
|
+
|
23
|
+
def initialize args
|
24
|
+
@opts, extra_args = parse_options(args)
|
25
|
+
@config = Configuration.current
|
26
|
+
|
27
|
+
@runner = config.runner
|
28
|
+
@logger = config.logger
|
29
|
+
@extra_args = extra_args
|
30
|
+
end
|
31
|
+
|
32
|
+
def parse_options args
|
33
|
+
opts = {
|
34
|
+
force: false
|
35
|
+
}
|
36
|
+
|
37
|
+
args = OptionParser.new do |parser|
|
38
|
+
parser.banner = "Usage: mau [options] APPLICATION_NAME"
|
39
|
+
parser.separator ""
|
40
|
+
parser.separator "Specific options:"
|
41
|
+
|
42
|
+
parser.on('-r', '--ref REF', 'Target git reference to update to') do |v|
|
43
|
+
opts[:ref] = v
|
44
|
+
end.on('-f', '--force', 'Forces update') do |v|
|
45
|
+
opts[:force] = true
|
46
|
+
end.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
47
|
+
opts[:verbose] = v
|
48
|
+
end.on_tail("-h", "--help", "Show this message") do
|
49
|
+
puts parser
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
end.parse(args)
|
53
|
+
|
54
|
+
return opts, args
|
55
|
+
end
|
56
|
+
|
57
|
+
def run
|
58
|
+
# If extra_args is empty, we need to look at all apps that were deployed
|
59
|
+
# here before.
|
60
|
+
app_names = extra_args
|
61
|
+
|
62
|
+
if app_names.empty?
|
63
|
+
begin
|
64
|
+
app_names = IO.readlines(applications_path).map(&:chomp)
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
warn "Could not read application names from #{APP_CONFIG}."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Iterate over all applications, updating every one of them.
|
71
|
+
successful_apps = app_names.select do |name|
|
72
|
+
run_for_app name
|
73
|
+
end
|
74
|
+
|
75
|
+
# Keep track of what applications are deployed here.
|
76
|
+
File.write applications_path, successful_apps.join("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
def run_for_app name
|
80
|
+
info "Attempting update of #{name}."
|
81
|
+
app(name).update
|
82
|
+
info "Done. (updating #{name})"
|
83
|
+
|
84
|
+
return true
|
85
|
+
rescue
|
86
|
+
error "Failed to update #{name}, see exception for details."
|
87
|
+
raise
|
88
|
+
end
|
89
|
+
|
90
|
+
# ----------------------------------------------------------------- internal
|
91
|
+
|
92
|
+
def app(name)
|
93
|
+
Application.new(name, config, opts)
|
94
|
+
end
|
95
|
+
def applications_path
|
96
|
+
config.app_base_path(APP_CONFIG)
|
97
|
+
end
|
98
|
+
|
99
|
+
def panic message
|
100
|
+
puts message
|
101
|
+
fatal message
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
|
105
|
+
delegate :debug, :info, :warn, :error, :fatal,
|
106
|
+
to: :logger
|
107
|
+
|
108
|
+
delegate :shell, :shell_as,
|
109
|
+
to: :runner
|
110
|
+
end
|
data/lib/application.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'delegation'
|
2
|
+
|
3
|
+
class Application
|
4
|
+
extend Delegation
|
5
|
+
|
6
|
+
def initialize(name, configuration, options={})
|
7
|
+
@runner = configuration.runner
|
8
|
+
@logger = configuration.logger
|
9
|
+
@configuration = configuration
|
10
|
+
@options = options
|
11
|
+
|
12
|
+
@name = name
|
13
|
+
@base_path = configuration.app_base_path(name)
|
14
|
+
@git_repo = "git@github.com:mobino/#{name}.git"
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :options
|
18
|
+
attr_reader :logger
|
19
|
+
attr_reader :name
|
20
|
+
attr_reader :configuration
|
21
|
+
attr_reader :base_path
|
22
|
+
attr_reader :git_repo
|
23
|
+
|
24
|
+
def update
|
25
|
+
# initial_checkout returns true if it performs work.
|
26
|
+
nothing_there = initial_checkout
|
27
|
+
|
28
|
+
# Save this flag now, since updating the code will modify it.
|
29
|
+
fetch_remote
|
30
|
+
update_requested = update_requested?
|
31
|
+
|
32
|
+
if nothing_there || update_requested
|
33
|
+
update_application
|
34
|
+
end
|
35
|
+
|
36
|
+
update_configuration
|
37
|
+
|
38
|
+
db_create if nothing_there
|
39
|
+
db_migrate if update_requested
|
40
|
+
end
|
41
|
+
|
42
|
+
# Assuming that nothing is there except the application directory (below
|
43
|
+
# /srv) usually, this will do the initial checkout.
|
44
|
+
#
|
45
|
+
def initial_checkout
|
46
|
+
# Prepare app environment, if needed.
|
47
|
+
unless current.directory? && shared.directory?
|
48
|
+
FileUtils.mkdir_p current
|
49
|
+
FileUtils.chown_R 'app', 'app', current
|
50
|
+
|
51
|
+
FileUtils.mkdir_p shared('tmp', 'pids')
|
52
|
+
FileUtils.mkdir_p shared('log')
|
53
|
+
FileUtils.chown_R 'app', 'app', shared
|
54
|
+
end
|
55
|
+
|
56
|
+
return false if current('.git').directory?
|
57
|
+
|
58
|
+
panic "No checkout in place and no git reference to update to given. Use '--ref'. " \
|
59
|
+
unless ref
|
60
|
+
|
61
|
+
info "Performing a complete initial installation."
|
62
|
+
|
63
|
+
shell "git clone #{git_repo} ."
|
64
|
+
shell "git checkout -B deployed #{ref}"
|
65
|
+
|
66
|
+
File.write path('.ref'), ref
|
67
|
+
|
68
|
+
info "Done. (initial installation)"
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fetches the remote refs for the current application. This will not change
|
73
|
+
# the currently deployed branch, just makes sure that the repository is up
|
74
|
+
# to date.
|
75
|
+
#
|
76
|
+
def fetch_remote
|
77
|
+
shell "git fetch origin"
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_requested?
|
81
|
+
shell("git diff --shortstat #{ref} deployed") != "" ||
|
82
|
+
options[:force]
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_application
|
86
|
+
info "Performing an app update."
|
87
|
+
|
88
|
+
# Write down the update intention (so we can repeat this easily)
|
89
|
+
File.write path('.ref'), ref
|
90
|
+
|
91
|
+
# Then try to update
|
92
|
+
shell "git reset --hard #{ref}"
|
93
|
+
|
94
|
+
# Install gems
|
95
|
+
shell "bundle install --deployment \
|
96
|
+
--without test spec development cucumber mac jruby"
|
97
|
+
|
98
|
+
# Create a few directories that the app also wants.
|
99
|
+
%w(tmp log).each do |dir_name|
|
100
|
+
begin
|
101
|
+
FileUtils.ln_sf shared(dir_name), current
|
102
|
+
rescue Errno::EEXIST
|
103
|
+
warn "Target directory (#{dir_name}) already exists."
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
info "Done (app update)."
|
108
|
+
end
|
109
|
+
|
110
|
+
def update_configuration
|
111
|
+
info "Updating configuration files (symlinks)."
|
112
|
+
|
113
|
+
# Link the configuration files from app base. (base -> current('config'))
|
114
|
+
%w(*.yml *.rb).each do |glob|
|
115
|
+
Dir[path(glob)].each do |override_file|
|
116
|
+
FileUtils.ln_sf override_file, current('config')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Make sure that whatever owners the files had, they now belong to 'app'.
|
121
|
+
FileUtils.chown_R 'app', 'app', current('config')
|
122
|
+
end
|
123
|
+
|
124
|
+
def db_migrate
|
125
|
+
info "Attempting database migration."
|
126
|
+
|
127
|
+
# Try to update the database
|
128
|
+
shell "bundle exec rake db:migrate"
|
129
|
+
|
130
|
+
info "Done (db migration)."
|
131
|
+
rescue Runner::CommandFailed
|
132
|
+
warn "Could not migrate the database schema."
|
133
|
+
end
|
134
|
+
def db_create
|
135
|
+
info "Attempting database creation & seeding."
|
136
|
+
|
137
|
+
# Try to update the database
|
138
|
+
shell "bundle exec rake db:create"
|
139
|
+
shell 'bundle exec rake db:seed || true'
|
140
|
+
|
141
|
+
info "Done (db creation & seed)."
|
142
|
+
rescue Runner::CommandFailed
|
143
|
+
warn "Could not migrate the database schema."
|
144
|
+
end
|
145
|
+
|
146
|
+
def path *args
|
147
|
+
base_path.join(*args)
|
148
|
+
end
|
149
|
+
|
150
|
+
def current *args
|
151
|
+
path 'current', *args
|
152
|
+
end
|
153
|
+
def shared *args
|
154
|
+
path 'shared', *args
|
155
|
+
end
|
156
|
+
|
157
|
+
def ref
|
158
|
+
options[:ref] || target_ref
|
159
|
+
end
|
160
|
+
|
161
|
+
# Runs a command in a subshell in the #current directory. As user 'app'.
|
162
|
+
#
|
163
|
+
def shell cmd, opts={}
|
164
|
+
@runner.shell_as 'app', cmd, {cwd: current}.merge(opts)
|
165
|
+
end
|
166
|
+
|
167
|
+
def target_ref
|
168
|
+
ref_path = path('.ref')
|
169
|
+
if ref_path.file?
|
170
|
+
return File.read(ref_path).strip
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
delegate :debug, :info, :warn, :error, :fatal,
|
175
|
+
to: :logger
|
176
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
class Configuration
|
5
|
+
class << self
|
6
|
+
def reset; @current = nil; end
|
7
|
+
def current; @current ||= new; end
|
8
|
+
def method_missing(sym, *args, &block)
|
9
|
+
return current.send(sym, *args, &block) if current.respond_to?(sym)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@log_file = $stderr
|
16
|
+
@app_base = '/srv'
|
17
|
+
|
18
|
+
@logger = Logger.new(log_file)
|
19
|
+
@runner = Runner.new(logger)
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :log_file
|
23
|
+
attr_accessor :app_base
|
24
|
+
attr_accessor :runner
|
25
|
+
attr_reader :logger
|
26
|
+
|
27
|
+
def log_file=(file)
|
28
|
+
@log_file = file
|
29
|
+
@logger = Logger.new(file)
|
30
|
+
end
|
31
|
+
|
32
|
+
def app_base_path *args
|
33
|
+
Pathname.new File.join(@app_base, *args)
|
34
|
+
end
|
35
|
+
end
|
data/lib/delegation.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module Delegation
|
3
|
+
# Defines a delegation to another object. Use this as follows:
|
4
|
+
#
|
5
|
+
# delegate_to :a, :b, to: :foobar
|
6
|
+
#
|
7
|
+
# This will delegate local methods 'a' and 'b' to the object returned by
|
8
|
+
# the accessor :foobar. Calling
|
9
|
+
#
|
10
|
+
# self.a('test')
|
11
|
+
#
|
12
|
+
# will now really call
|
13
|
+
#
|
14
|
+
# self.foobar.a('test')
|
15
|
+
#
|
16
|
+
def delegate(*arguments)
|
17
|
+
opts = arguments.pop
|
18
|
+
|
19
|
+
raise ArgumentError, "Missing options hash at end of delegate arguments." \
|
20
|
+
unless opts && opts[:to]
|
21
|
+
|
22
|
+
to_ref = opts[:to]
|
23
|
+
silent = opts[:silent]
|
24
|
+
|
25
|
+
arguments.each do |name|
|
26
|
+
define_method(name) do |*args, &block|
|
27
|
+
obj = self.send(to_ref)
|
28
|
+
|
29
|
+
fail "Delegation to nil object. (#{to_ref} - #{name})." \
|
30
|
+
if !obj && !silent
|
31
|
+
|
32
|
+
obj.send(name, *args, &block) if obj
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/runner.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
require 'mixlib/shellout'
|
3
|
+
|
4
|
+
require 'delegation'
|
5
|
+
|
6
|
+
class Runner
|
7
|
+
extend Delegation
|
8
|
+
|
9
|
+
def initialize(logger=nil)
|
10
|
+
@logger = logger
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :logger
|
14
|
+
|
15
|
+
class CommandFailed < StandardError; end
|
16
|
+
|
17
|
+
def shell cmd, opts={}
|
18
|
+
cmd = cmd.strip
|
19
|
+
|
20
|
+
# As a default, escape from the bundler jail:
|
21
|
+
environment = opts[:environment] ||= {}
|
22
|
+
environment['RUBYOPT'] = ''
|
23
|
+
environment['BUNDLE_GEMFILE'] = ''
|
24
|
+
environment['RAILS_ENV'] = 'production'
|
25
|
+
environment['RACK_ENV'] = 'production'
|
26
|
+
|
27
|
+
debug "Executing '#{cmd}'."
|
28
|
+
command = shell_out(cmd, opts)
|
29
|
+
|
30
|
+
command.run_command
|
31
|
+
command.error!
|
32
|
+
|
33
|
+
return command.stdout
|
34
|
+
rescue Mixlib::ShellOut::ShellCommandFailed => error
|
35
|
+
warn "Failed command (exit #{command.exitstatus}): #{cmd} "
|
36
|
+
|
37
|
+
lines = command.stderr.lines.to_a
|
38
|
+
warn "STDERR: #{lines.size} lines of output:" unless lines.empty?
|
39
|
+
lines.each_with_index do |line, idx|
|
40
|
+
symbol = idx == lines.size-1 ? '`-' : '|-'
|
41
|
+
debug " #{symbol} #{line.chomp}"
|
42
|
+
end
|
43
|
+
|
44
|
+
lines = command.stdout.lines.to_a
|
45
|
+
warn "STDOUT: #{lines.size} lines of output:" unless lines.empty?
|
46
|
+
lines.each_with_index do |line, idx|
|
47
|
+
symbol = idx == lines.size-1 ? '`-' : '|-'
|
48
|
+
debug " #{symbol} #{line.chomp}"
|
49
|
+
end
|
50
|
+
|
51
|
+
raise CommandFailed, "'#{cmd}' exited with #{command.exitstatus}. (see log for details)"
|
52
|
+
end
|
53
|
+
def shell_as user, cmd, opts={}
|
54
|
+
shell cmd, opts.merge(user: user)
|
55
|
+
end
|
56
|
+
|
57
|
+
def shell_out(*args)
|
58
|
+
Mixlib::ShellOut.new(*args)
|
59
|
+
end
|
60
|
+
|
61
|
+
delegate :debug, :info, :warn, :error, :fatal,
|
62
|
+
to: :logger, silent: true
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mau
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Appleseed
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mixlib-shellout
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description:
|
31
|
+
email: contact@mobino.com
|
32
|
+
executables:
|
33
|
+
- mau
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files:
|
36
|
+
- README
|
37
|
+
files:
|
38
|
+
- README
|
39
|
+
- lib/app_update.rb
|
40
|
+
- lib/application.rb
|
41
|
+
- lib/configuration.rb
|
42
|
+
- lib/delegation.rb
|
43
|
+
- lib/runner.rb
|
44
|
+
- bin/mau
|
45
|
+
homepage:
|
46
|
+
licenses: []
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --main
|
50
|
+
- README
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.24
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Update applications on hosts from a git reference.
|
71
|
+
test_files: []
|