mau 0.1.1

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.
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: []