mau 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,5 @@
1
+
2
+ Updates rails and non-rails applications on a server from a git reference.
3
+
4
+ SYNOPSIS
5
+
data/bin/mau ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Updates an application on one of the servers.
4
+
5
+ # 'lib' into load path
6
+ $:.unshift File.expand_path(
7
+ File.join(File.dirname(__FILE__), '..', 'lib'))
8
+
9
+ require 'app_update'
10
+ AppUpdate.new(ARGV).run
@@ -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
@@ -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
@@ -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
@@ -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: []