slugforge 4.0.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.
- checksums.yaml +7 -0
- data/README.md +316 -0
- data/bin/slugforge +9 -0
- data/lib/slugforge.rb +19 -0
- data/lib/slugforge/build.rb +4 -0
- data/lib/slugforge/build/build_project.rb +31 -0
- data/lib/slugforge/build/export_upstart.rb +85 -0
- data/lib/slugforge/build/package.rb +63 -0
- data/lib/slugforge/cli.rb +125 -0
- data/lib/slugforge/commands.rb +130 -0
- data/lib/slugforge/commands/build.rb +20 -0
- data/lib/slugforge/commands/config.rb +24 -0
- data/lib/slugforge/commands/deploy.rb +383 -0
- data/lib/slugforge/commands/project.rb +21 -0
- data/lib/slugforge/commands/tag.rb +148 -0
- data/lib/slugforge/commands/wrangler.rb +142 -0
- data/lib/slugforge/configuration.rb +125 -0
- data/lib/slugforge/helper.rb +186 -0
- data/lib/slugforge/helper/build.rb +46 -0
- data/lib/slugforge/helper/config.rb +37 -0
- data/lib/slugforge/helper/enumerable.rb +46 -0
- data/lib/slugforge/helper/fog.rb +90 -0
- data/lib/slugforge/helper/git.rb +89 -0
- data/lib/slugforge/helper/path.rb +76 -0
- data/lib/slugforge/helper/project.rb +86 -0
- data/lib/slugforge/models/host.rb +233 -0
- data/lib/slugforge/models/host/fog_host.rb +33 -0
- data/lib/slugforge/models/host/hostname_host.rb +9 -0
- data/lib/slugforge/models/host/ip_address_host.rb +9 -0
- data/lib/slugforge/models/host_group.rb +65 -0
- data/lib/slugforge/models/host_group/aws_tag_group.rb +22 -0
- data/lib/slugforge/models/host_group/ec2_instance_group.rb +21 -0
- data/lib/slugforge/models/host_group/hostname_group.rb +16 -0
- data/lib/slugforge/models/host_group/ip_address_group.rb +16 -0
- data/lib/slugforge/models/host_group/security_group_group.rb +20 -0
- data/lib/slugforge/models/logger.rb +36 -0
- data/lib/slugforge/models/tag_manager.rb +125 -0
- data/lib/slugforge/slugins.rb +125 -0
- data/lib/slugforge/version.rb +9 -0
- data/scripts/post-install.sh +143 -0
- data/scripts/unicorn-shepherd.sh +305 -0
- data/spec/fixtures/array.yaml +3 -0
- data/spec/fixtures/fog_credentials.yaml +4 -0
- data/spec/fixtures/invalid_syntax.yaml +1 -0
- data/spec/fixtures/one.yaml +3 -0
- data/spec/fixtures/two.yaml +3 -0
- data/spec/fixtures/valid.yaml +4 -0
- data/spec/slugforge/commands/deploy_spec.rb +72 -0
- data/spec/slugforge/commands_spec.rb +33 -0
- data/spec/slugforge/configuration_spec.rb +200 -0
- data/spec/slugforge/helper/fog_spec.rb +81 -0
- data/spec/slugforge/helper/git_spec.rb +152 -0
- data/spec/slugforge/models/host_group/aws_tag_group_spec.rb +54 -0
- data/spec/slugforge/models/host_group/ec2_instance_group_spec.rb +51 -0
- data/spec/slugforge/models/host_group/hostname_group_spec.rb +20 -0
- data/spec/slugforge/models/host_group/ip_address_group_spec.rb +54 -0
- data/spec/slugforge/models/host_group/security_group_group_spec.rb +52 -0
- data/spec/slugforge/models/tag_manager_spec.rb +75 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/env.rb +3 -0
- data/spec/support/example_groups/configuration_writer.rb +24 -0
- data/spec/support/example_groups/helper_provider.rb +10 -0
- data/spec/support/factories.rb +18 -0
- data/spec/support/fog.rb +15 -0
- data/spec/support/helpers.rb +18 -0
- data/spec/support/mock_logger.rb +6 -0
- data/spec/support/ssh.rb +8 -0
- data/spec/support/streams.rb +13 -0
- data/templates/foreman/master.conf.erb +21 -0
- data/templates/foreman/process-master.conf.erb +2 -0
- data/templates/foreman/process.conf.erb +19 -0
- metadata +344 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
module Slugforge
|
2
|
+
class Logger
|
3
|
+
def initialize(thor_shell, log_level = :info)
|
4
|
+
@thor_shell = thor_shell
|
5
|
+
@log_level = log_level
|
6
|
+
end
|
7
|
+
|
8
|
+
def log(message="", opts={})
|
9
|
+
return if @log_level != :verbose && opts[:log_level] == :verbose
|
10
|
+
if opts[:status]
|
11
|
+
say_status opts[:status], message, opts[:color]
|
12
|
+
else
|
13
|
+
if opts[:force_new_line]
|
14
|
+
say message, opts[:color], true
|
15
|
+
else
|
16
|
+
say message, opts[:color]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)\z/))
|
22
|
+
return if [:quiet, :json].include?(@log_level)
|
23
|
+
@thor_shell.say message, color, force_new_line
|
24
|
+
end
|
25
|
+
|
26
|
+
def say_status(status, message, log_status=true)
|
27
|
+
return if [:quiet, :json].include?(@log_level)
|
28
|
+
@thor_shell.say_status status, message, log_status
|
29
|
+
end
|
30
|
+
|
31
|
+
def say_json(message)
|
32
|
+
return unless @log_level == :json
|
33
|
+
@thor_shell.say message.to_json
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Slugforge
|
2
|
+
class TagManager
|
3
|
+
def initialize(opts)
|
4
|
+
bucket(opts)
|
5
|
+
end
|
6
|
+
|
7
|
+
def bucket(opts={})
|
8
|
+
if @bucket.nil? || (true == (opts[:refresh] || @bucket_dirty))
|
9
|
+
@s3 = opts[:s3] || @s3
|
10
|
+
@aws_bucket = opts[:bucket] || @aws_bucket
|
11
|
+
@bucket = @s3.directories.get(@aws_bucket)
|
12
|
+
@slugs_for_tag = {}
|
13
|
+
@tags = {}
|
14
|
+
@bucket_dirty = false
|
15
|
+
end
|
16
|
+
@bucket
|
17
|
+
end
|
18
|
+
|
19
|
+
def projects
|
20
|
+
return [] if bucket.files.nil?
|
21
|
+
result = {}
|
22
|
+
bucket.files.each do |file|
|
23
|
+
result[$~[1]] = true if (file.key =~ /^([^\/]+)\//)
|
24
|
+
end
|
25
|
+
result.keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def tags(project_name)
|
29
|
+
@tags[project_name] ||= begin
|
30
|
+
return [] if bucket.files.nil?
|
31
|
+
result = {}
|
32
|
+
bucket.files.each do |file|
|
33
|
+
result[$~[1]] = true if file.key =~ /^#{project_name}\/tags\/(.+)/
|
34
|
+
end
|
35
|
+
result.keys
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# This method should be called before iterating over a large set of slugs and
|
40
|
+
# calling #tags_for_slug on them. By doing this you are able to query all the
|
41
|
+
# data from AWS in advance using parallelized threads, rather than in serial.
|
42
|
+
def memoize_slugs_for_tags(project_name)
|
43
|
+
@slugs_for_tag[project_name] ||= {}
|
44
|
+
tag_map = tags(project_name).parallel_map do |tag|
|
45
|
+
next if @slugs_for_tag[project_name][tag]
|
46
|
+
file = nil
|
47
|
+
begin
|
48
|
+
file = bucket.files.get(tag_file_name(project_name, tag))
|
49
|
+
rescue Excon::Errors::Forbidden
|
50
|
+
# ignore 403's
|
51
|
+
end
|
52
|
+
slugs = file.nil? ? [] : file.body.split("\n")
|
53
|
+
[tag, slugs]
|
54
|
+
end
|
55
|
+
tag_map.each do |tag, slugs|
|
56
|
+
@slugs_for_tag[project_name][tag] = slugs
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def slugs_for_tag(project_name, tag)
|
61
|
+
@slugs_for_tag[project_name] ||= {}
|
62
|
+
@slugs_for_tag[project_name][tag] ||= begin
|
63
|
+
return [] if bucket.files.nil?
|
64
|
+
file = nil
|
65
|
+
begin
|
66
|
+
file = bucket.files.get(tag_file_name(project_name, tag))
|
67
|
+
rescue Excon::Errors::Forbidden
|
68
|
+
# ignore 403's
|
69
|
+
end
|
70
|
+
file.nil? ? [] : file.body.split("\n")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def rollback_slug_for_tag(project_name, tag)
|
75
|
+
slugs = slugs_for_tag(project_name, tag)
|
76
|
+
slugs.shift
|
77
|
+
save_tag(project_name, tag, slugs) unless slugs.empty?
|
78
|
+
slugs.first
|
79
|
+
end
|
80
|
+
|
81
|
+
def slug_for_tag(project_name, tag)
|
82
|
+
slugs = slugs_for_tag(project_name, tag)
|
83
|
+
slugs.first
|
84
|
+
end
|
85
|
+
|
86
|
+
def tags_for_slug(project_name, slug_name)
|
87
|
+
tags = tags(project_name)
|
88
|
+
|
89
|
+
tags.select do |tag|
|
90
|
+
slug_for_tag(project_name, tag) == slug_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def clone_tag(project_name, old_tag, new_tag)
|
95
|
+
slugs = slugs_for_tag(project_name, old_tag)
|
96
|
+
save_tag(project_name, new_tag, slugs)
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_tag(project_name, tag, slug_name)
|
100
|
+
slugs = [slug_name]
|
101
|
+
slugs += slugs_for_tag(project_name, tag)
|
102
|
+
slugs = slugs.slice(0,10)
|
103
|
+
save_tag(project_name, tag, slugs)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete_tag(project_name, tag)
|
107
|
+
return nil if bucket.files.nil?
|
108
|
+
bucket.files.head(tag_file_name(project_name, tag)).destroy
|
109
|
+
@bucket_dirty = true
|
110
|
+
end
|
111
|
+
|
112
|
+
def save_tag(project_name, tag, slugs)
|
113
|
+
bucket.files.create(
|
114
|
+
:key => tag_file_name(project_name, tag),
|
115
|
+
:body => slugs.join("\n"),
|
116
|
+
:public => false
|
117
|
+
)
|
118
|
+
@bucket_dirty = true
|
119
|
+
end
|
120
|
+
|
121
|
+
def tag_file_name(project_name, tag)
|
122
|
+
[project_name, 'tags', tag].join('/')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# Copied from Pry under the terms of the MIT license.
|
2
|
+
# https://github.com/pry/pry/blob/0f207450a968e9e72b6e8cc8b2c21e7029569d3b/lib/pry/slugins.rb
|
3
|
+
|
4
|
+
module Slugforge
|
5
|
+
class SluginManager
|
6
|
+
PREFIX = /^slugforge-/
|
7
|
+
|
8
|
+
# Placeholder when no associated gem found, displays warning
|
9
|
+
class NoSlugin
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(*args)
|
15
|
+
warn "Warning: The slugin '#{@name}' was not found! (no gem found)"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Slugin
|
20
|
+
attr_accessor :name, :gem_name, :enabled, :spec, :active
|
21
|
+
|
22
|
+
def initialize(name, gem_name, spec, enabled)
|
23
|
+
@name, @gem_name, @enabled, @spec = name, gem_name, enabled, spec
|
24
|
+
end
|
25
|
+
|
26
|
+
# Disable a slugin. (prevents slugin from being loaded, cannot
|
27
|
+
# disable an already activated slugin)
|
28
|
+
def disable!
|
29
|
+
self.enabled = false
|
30
|
+
end
|
31
|
+
|
32
|
+
# Enable a slugin. (does not load it immediately but puts on
|
33
|
+
# 'white list' to be loaded)
|
34
|
+
def enable!
|
35
|
+
self.enabled = true
|
36
|
+
end
|
37
|
+
|
38
|
+
# Load the slugin (require the gem - enables/loads the
|
39
|
+
# slugin immediately at point of call, even if slugin is
|
40
|
+
# disabled)
|
41
|
+
# Does not reload slugin if it's already loaded.
|
42
|
+
def load!
|
43
|
+
begin
|
44
|
+
require gem_name
|
45
|
+
rescue LoadError => e
|
46
|
+
warn "Found slugin #{gem_name}, but could not require '#{gem_name}.rb'"
|
47
|
+
warn e
|
48
|
+
rescue => e
|
49
|
+
warn "require '#{gem_name}' failed, saying: #{e}"
|
50
|
+
end
|
51
|
+
|
52
|
+
self.enabled = true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Activate the slugin (run its defined activation method)
|
56
|
+
# Does not reactivate if already active.
|
57
|
+
def activate!(config)
|
58
|
+
return if active?
|
59
|
+
|
60
|
+
if klass = slugin_class
|
61
|
+
klass.activate(config) if klass.respond_to?(:activate)
|
62
|
+
end
|
63
|
+
|
64
|
+
self.active = true
|
65
|
+
end
|
66
|
+
|
67
|
+
alias active? active
|
68
|
+
alias enabled? enabled
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def slugin_class
|
73
|
+
name = spec.name.gsub(/^slugforge-/, '').camelize
|
74
|
+
name = "Slugforge#{name}"
|
75
|
+
begin
|
76
|
+
name.constantize
|
77
|
+
rescue NameError
|
78
|
+
warn "Slugin #{gem_name} cannot be activated. Expected module named #{name}."
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize
|
84
|
+
@slugins = []
|
85
|
+
locate_slugins
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [Hash] A hash with all slugin names (minus the prefix) as
|
89
|
+
# keys and slugin objects as values.
|
90
|
+
def slugins
|
91
|
+
h = Hash.new { |_, key| NoSlugin.new(key) }
|
92
|
+
@slugins.each do |slugin|
|
93
|
+
h[slugin.name] = slugin
|
94
|
+
end
|
95
|
+
h
|
96
|
+
end
|
97
|
+
|
98
|
+
# Require all enabled slugins, disabled slugins are skipped.
|
99
|
+
def load_slugins
|
100
|
+
@slugins.each(&:load!)
|
101
|
+
end
|
102
|
+
|
103
|
+
def activate_slugins(config)
|
104
|
+
@slugins.each { |s| s.activate!(config) if s.enabled? }
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# Find all installed Pry slugins and store them in an internal array.
|
110
|
+
def locate_slugins
|
111
|
+
Gem.refresh
|
112
|
+
(Gem::Specification.respond_to?(:each) ? Gem::Specification : Gem.source_index.find_name('')).each do |gem|
|
113
|
+
next if gem.name !~ PREFIX
|
114
|
+
slugin_name = gem.name.split('-', 2).last
|
115
|
+
@slugins << Slugin.new(slugin_name, gem.name, gem, true) if !gem_located?(gem.name)
|
116
|
+
end
|
117
|
+
@slugins
|
118
|
+
end
|
119
|
+
|
120
|
+
def gem_located?(gem_name)
|
121
|
+
@slugins.any? { |slugin| slugin.gem_name == gem_name }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Environment variables can be used to modify the behavior of this script. They
|
4
|
+
# must be present in the environment of the slug when it is installed
|
5
|
+
#
|
6
|
+
# KILL_TIMEOUT - change the upstart kill timeout which is how long upstart will
|
7
|
+
# wait upon stopping a service for child processes to die before
|
8
|
+
# sending kill -9
|
9
|
+
# CONCURRENCY - Concurrency used for foreman export. Same format as concurrency
|
10
|
+
# argument passed for foreman
|
11
|
+
# RUNTIME_RUBY_VERSION - The ruby version to put in the upstart templates to run the service.
|
12
|
+
# If specified 'rvm use $RUNTIME_RUBY_VERSION do' will prefix the process
|
13
|
+
# command in the Procfile
|
14
|
+
# LOGROTATE_POSTROTATE - The shell code to run in the logrotate 'postrotate' stanza
|
15
|
+
# in the logrotate config file created for this slug's upstart
|
16
|
+
# service. Default is 'restart <servicename>'
|
17
|
+
|
18
|
+
set -e
|
19
|
+
|
20
|
+
SHARED_DIR="${INSTALL_ROOT}/shared"
|
21
|
+
# make shared folders, if needed
|
22
|
+
mkdir -p ${SHARED_DIR}/config
|
23
|
+
|
24
|
+
linked_dirs=(log pids tmp)
|
25
|
+
|
26
|
+
for d in ${linked_dirs[@]} ; do
|
27
|
+
mkdir -p "${SHARED_DIR}/${d}"
|
28
|
+
# create the symlinks for shared folders, if needed
|
29
|
+
if [ ! -h "$INSTALL_DIR/${d}" ] ; then
|
30
|
+
rm -rf "$INSTALL_DIR/${d}" # delete local copy, use shared
|
31
|
+
ln -s -f "${SHARED_DIR}/${d}" "${INSTALL_DIR}/${d}"
|
32
|
+
fi
|
33
|
+
done
|
34
|
+
chmod -R 775 ${SHARED_DIR}
|
35
|
+
|
36
|
+
# set owner for project tree
|
37
|
+
chown -R $OWNER ${INSTALL_ROOT}
|
38
|
+
|
39
|
+
# make sure all deploy scripts are executable
|
40
|
+
if [ -d "${INSTALL_DIR}/deploy" ] ; then
|
41
|
+
chmod +x ${INSTALL_DIR}/deploy/*
|
42
|
+
fi
|
43
|
+
|
44
|
+
# if environment file exists, link it into current directory so DotEnv and foreman run work
|
45
|
+
if [ -r "${SHARED_DIR}/env" ] ; then
|
46
|
+
ln -s -f "${SHARED_DIR}/env" "${INSTALL_DIR}/.env"
|
47
|
+
fi
|
48
|
+
|
49
|
+
# run post_install script, if present
|
50
|
+
if [ -r "${INSTALL_DIR}/deploy/post_install" ] ; then
|
51
|
+
echo "Running post_install script..."
|
52
|
+
# change into INSTALL_DIR so that folks can use pwd to get package install location
|
53
|
+
su - $OWNER -c "cd ${INSTALL_DIR}; deploy/post_install"
|
54
|
+
fi
|
55
|
+
|
56
|
+
if which service && which start && which stop > /dev/null 2>&1 ; then
|
57
|
+
UPSTART_PRESENT=true
|
58
|
+
else
|
59
|
+
UPSTART_PRESENT=false
|
60
|
+
fi
|
61
|
+
|
62
|
+
if $UPSTART_PRESENT ; then
|
63
|
+
if [ -n "$CONCURRENCY" ] ; then
|
64
|
+
CONCURRENCY="-c $CONCURRENCY"
|
65
|
+
|
66
|
+
# split up the concurrency string into its parts and check each app to see if its
|
67
|
+
# unicorn or rainbows. We can only have one at a time because if they share a pid
|
68
|
+
# directory unicorn-upstart can't tell which process to watch.
|
69
|
+
# e.g.
|
70
|
+
# web=1,other=1 gets split on the command then app gets split on the = to be web and other
|
71
|
+
FOUND_UNICORN=false
|
72
|
+
for app in $(echo ${CONCURRENCY/,/ }) ; do
|
73
|
+
app=${app%=*}
|
74
|
+
if egrep -q "^${app}:.*(unicorn|rainbows)" "${INSTALL_DIR}/Procfile" ; then
|
75
|
+
if $FOUND_UNICORN ; then
|
76
|
+
echo "The concurrency you have set of '$CONCURRENCY' will result in two unicorn or rainbows servers running at the same time. Slug deploys do not support that."
|
77
|
+
echo "Update your concurrency to only run one. You can deploy also this slug again in another directory with a different concurrency to run both simultaneously."
|
78
|
+
exit 1
|
79
|
+
fi
|
80
|
+
FOUND_UNICORN=true
|
81
|
+
fi
|
82
|
+
done
|
83
|
+
fi
|
84
|
+
|
85
|
+
PROJECT_NAME=$(basename $INSTALL_ROOT)
|
86
|
+
|
87
|
+
# upstart has problems with services with dashes in them
|
88
|
+
PROJECT_NAME=${PROJECT_NAME/-/_}
|
89
|
+
|
90
|
+
if [ -n "$RUNTIME_RUBY_VERSION" ] ; then
|
91
|
+
# used inside the foreman template
|
92
|
+
export RUBY_CMD="rvm use $RUNTIME_RUBY_VERSION do"
|
93
|
+
elif [ -r "${INSTALL_DIR}/.ruby-version" ] ; then
|
94
|
+
export RUBY_CMD="rvm use $(head -n 1 ${INSTALL_DIR}/.ruby-version) do"
|
95
|
+
fi
|
96
|
+
|
97
|
+
# check for a Procfile that is not zero size
|
98
|
+
if [ -s "$INSTALL_DIR/Procfile" ] ; then
|
99
|
+
# run foreman export to export the upstart scripts
|
100
|
+
EXPORT_COMMAND="foreman export upstart /etc/init -a $PROJECT_NAME -f $INSTALL_DIR/Procfile -l $INSTALL_DIR/log $CONCURRENCY -t $INSTALL_DIR/deploy/upstart-templates -d $INSTALL_ROOT -u $OWNER"
|
101
|
+
echo "Running foreman export command '$EXPORT_COMMAND'"
|
102
|
+
$EXPORT_COMMAND
|
103
|
+
|
104
|
+
# start or restart the service
|
105
|
+
if status ${PROJECT_NAME} | grep -q running ; then
|
106
|
+
# restart the service
|
107
|
+
echo "Post install complete. Restarting ${PROJECT_NAME} service... "
|
108
|
+
restart ${PROJECT_NAME}
|
109
|
+
else
|
110
|
+
# start the new service
|
111
|
+
echo "Post install complete. Starting ${PROJECT_NAME} service... "
|
112
|
+
start ${PROJECT_NAME}
|
113
|
+
fi
|
114
|
+
else
|
115
|
+
echo "Procfile is missing or zero size. *NOT* running foreman export command."
|
116
|
+
fi
|
117
|
+
else
|
118
|
+
echo "This machine does not appear to have upstart installed so we're skipping"
|
119
|
+
echo "exporting the upstart service config files."
|
120
|
+
fi
|
121
|
+
|
122
|
+
if [ -d "/etc/logrotate.d" ] ; then
|
123
|
+
LOGROTATE_FILE="/etc/logrotate.d/${PROJECT_NAME}"
|
124
|
+
LOG_DIR="${INSTALL_DIR}/log"
|
125
|
+
echo "Installing logrotate config ${LOGROTATE_FILE}"
|
126
|
+
: ${LOGROTATE_POSTROTATE:="restart ${PROJECT_NAME}"}
|
127
|
+
cat <<EOF > "${LOGROTATE_FILE}"
|
128
|
+
${LOG_DIR}/*log ${LOG_DIR}/*/*.log {
|
129
|
+
size=10G
|
130
|
+
rotate 2
|
131
|
+
missingok
|
132
|
+
notifempty
|
133
|
+
sharedscripts
|
134
|
+
postrotate
|
135
|
+
${LOGROTATE_POSTROTATE}
|
136
|
+
endscript
|
137
|
+
}
|
138
|
+
|
139
|
+
EOF
|
140
|
+
else
|
141
|
+
echo "This machine does not appear to have logrotate installed so we're skipping"
|
142
|
+
echo "log rotation config."
|
143
|
+
fi
|
@@ -0,0 +1,305 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# This script is a bridge between upstart and unicorn/rainbows, hereafter referred to as unicorn.
|
4
|
+
#
|
5
|
+
# The reason this is necessary is that upstart wants to start and watch a pid for its entire
|
6
|
+
# lifecycle. However, unicorn's cool no-downtime restart feature creates a new unicorn master
|
7
|
+
# which will create new workers and then kill the original master. This makes upstart think that
|
8
|
+
# unicorn died and it gets wonky from there.
|
9
|
+
#
|
10
|
+
# So this script is started by upstart. It can detect if a unicorn master is already running
|
11
|
+
# and will wait for it to exit. Then upstart will restart this script which will see if
|
12
|
+
# a unicorn master is running again. On no-downtime restarts it will find the new unicorn master
|
13
|
+
# and wait on it to exit, and so on. So unicorn is managing its own lifecycle and this script
|
14
|
+
# gives upstart a single pid to start and watch.
|
15
|
+
#
|
16
|
+
# This script also handles the signals sent by upstart to stop and restart and sends them to the
|
17
|
+
# running unicorn master to initiate a no-downtime restart when the upstart 'restart' command
|
18
|
+
# is given to this service.
|
19
|
+
#
|
20
|
+
# We do some crazy magic in is_restarting to determine if we are restarting or stopping.
|
21
|
+
|
22
|
+
|
23
|
+
#############################################################
|
24
|
+
##
|
25
|
+
## Set up environment
|
26
|
+
##
|
27
|
+
#############################################################
|
28
|
+
|
29
|
+
COMMAND=$1
|
30
|
+
SERVICE=$2
|
31
|
+
|
32
|
+
# logs to syslog with service name and the pid of this script
|
33
|
+
log() {
|
34
|
+
# we have to send this to syslog ourselves instead of relying on whoever launched
|
35
|
+
# us because the exit signal handler log output never shows up in the output stream
|
36
|
+
# unless we do this explicitly
|
37
|
+
echo "$@" | logger -t "${SERVICE}[$$]"
|
38
|
+
}
|
39
|
+
|
40
|
+
# assume upstart config cd's us into project root dir.
|
41
|
+
BASE_DIR=$PWD
|
42
|
+
LOG_DIR="${BASE_DIR}/log/unicorn"
|
43
|
+
TRY_RESTART=true
|
44
|
+
|
45
|
+
#############################################################
|
46
|
+
##
|
47
|
+
## Support functions
|
48
|
+
##
|
49
|
+
#############################################################
|
50
|
+
|
51
|
+
# Bail out if all is not well
|
52
|
+
check_environment(){
|
53
|
+
if [ "x" = "x${COMMAND}" ] ; then
|
54
|
+
log "Missing required argument: Command to launch unicorn or rainbows. [unicorn|rainbows]"
|
55
|
+
exit 1
|
56
|
+
fi
|
57
|
+
|
58
|
+
if [ "x" = "x${SERVICE}" ] ; then
|
59
|
+
log "Missing required second argument: Upstart service name that launched this script"
|
60
|
+
exit 1
|
61
|
+
fi
|
62
|
+
|
63
|
+
if [ -r $BASE_DIR/config/unicorn.rb ] ; then
|
64
|
+
CONFIG_FILE=$BASE_DIR/config/unicorn.rb
|
65
|
+
elif [ -r $BASE_DIR/config/rainbows.rb ] ; then
|
66
|
+
CONFIG_FILE=$BASE_DIR/config/rainbows.rb
|
67
|
+
else
|
68
|
+
log "No unicorn or rainbows config file found in '$BASE_DIR/config'. Exiting"
|
69
|
+
exit 1
|
70
|
+
fi
|
71
|
+
|
72
|
+
# default to RAILS_ENV if RACK_ENV isn't set
|
73
|
+
export RACK_ENV="${RACK_ENV:-$RAILS_ENV}"
|
74
|
+
|
75
|
+
if [ ! -n "$RACK_ENV" ] ; then
|
76
|
+
log "Neither RACK_ENV nor RAILS_ENV environment variable are set. Exiting."
|
77
|
+
exit 1
|
78
|
+
fi
|
79
|
+
|
80
|
+
}
|
81
|
+
|
82
|
+
# Return the pid of the new master unicorn. If there are two master unicorns running, not
|
83
|
+
# a new one and one marked old which is exiting, but two that think they are the master
|
84
|
+
# then exit with an error. How could we handle this better? When would it happen?
|
85
|
+
# Delete any pid files found which have no corresponding running processes.
|
86
|
+
master_pid() {
|
87
|
+
local pid=''
|
88
|
+
local extra_pids=''
|
89
|
+
local multi_master=false
|
90
|
+
|
91
|
+
for PID_FILE in $(find $BASE_DIR/pids/ -name "*.pid") ; do
|
92
|
+
local p=`cat ${PID_FILE}`
|
93
|
+
|
94
|
+
if is_pid_running $p ; then
|
95
|
+
if [ -n "$pid" ] ; then
|
96
|
+
multi_master=true
|
97
|
+
extra_pids="$extra_pids $p"
|
98
|
+
else
|
99
|
+
pid="$p"
|
100
|
+
fi
|
101
|
+
else
|
102
|
+
log "Deleting ${COMMAND} pid file with no running process '$PID_FILE'"
|
103
|
+
rm $PID_FILE 2> /dev/null || log "Failed to delete pid file '$PID_FILE': $!"
|
104
|
+
fi
|
105
|
+
done
|
106
|
+
if $multi_master ; then
|
107
|
+
log "Found more than one not old ${COMMAND} master process running. Pids are '$pid $extra_pids'."
|
108
|
+
log "Killing them all and restarting."
|
109
|
+
kill -9 $pid $extra_pids
|
110
|
+
exit 1
|
111
|
+
fi
|
112
|
+
|
113
|
+
echo $pid
|
114
|
+
# return status so we can use this function to see if the master is running
|
115
|
+
[ -n "$pid" ]
|
116
|
+
}
|
117
|
+
|
118
|
+
is_pid_running() {
|
119
|
+
local pid=$1
|
120
|
+
if [ ! -n "$pid" ] || ! [ -d "/proc/$pid" ] ; then
|
121
|
+
return 1
|
122
|
+
fi
|
123
|
+
return 0
|
124
|
+
}
|
125
|
+
|
126
|
+
|
127
|
+
# output parent process id of argument
|
128
|
+
ppid() {
|
129
|
+
ps -p $1 -o ppid=
|
130
|
+
}
|
131
|
+
|
132
|
+
free_mem() {
|
133
|
+
free -m | grep "buffers/cache:" | awk '{print $4};'
|
134
|
+
}
|
135
|
+
|
136
|
+
# kills off workers whose master have died. This is indicated by a worker whose
|
137
|
+
# parent process is the init process.
|
138
|
+
kill_orphaned_workers() {
|
139
|
+
local workers=`ps aux | egrep "${COMMAND}.*worker" | grep -v grep | awk '{print $2}'`
|
140
|
+
for worker in $workers ; do
|
141
|
+
# if the worker's parent process is init, its master is dead.
|
142
|
+
if [ "1" = `ppid $worker` ] ; then
|
143
|
+
log "Found ${COMMAND} worker process with no master. Killing $worker"
|
144
|
+
kill -QUIT $worker
|
145
|
+
fi
|
146
|
+
done
|
147
|
+
}
|
148
|
+
|
149
|
+
# This is the on exit handler. It checks if we are restarting or not and either sends the USR2
|
150
|
+
# signal to unicorn or, if the service is being stopped, kill the unicorn master.
|
151
|
+
respawn_new_master() {
|
152
|
+
# TRY_RESTART is set to false on exit where we didn't recieve TERM.
|
153
|
+
# When we used "trap command TERM" it did not always trap propertly
|
154
|
+
# but "trap command EXIT" runs command every time no matter why the script
|
155
|
+
# ends. So we set this env var to false if we don't need to respawn which is if unicorn
|
156
|
+
# dies by itself or is restarted externally, usually through the deploy script
|
157
|
+
# or we never succesfully started it.
|
158
|
+
# If we receive a TERM, like from upstart on stop/restart, this won't be set
|
159
|
+
# and we'll send USR2 to restart unicorn.
|
160
|
+
if $TRY_RESTART ; then
|
161
|
+
if is_service_in_state "restart" ; then
|
162
|
+
local pid=`master_pid`
|
163
|
+
if [ -n "$pid" ] ; then
|
164
|
+
# free memory before restart. Restart is unreliable with not enough memory.
|
165
|
+
# New master crashes during startup etc.
|
166
|
+
let min_mem=1500
|
167
|
+
let workers_to_kill=8
|
168
|
+
let count=0
|
169
|
+
|
170
|
+
while [ `free_mem` -lt $min_mem ] && [ $count -lt $workers_to_kill ] ; do
|
171
|
+
log "Sending master ${pid} TTOU to drop workers to free up memory for restart"
|
172
|
+
kill -TTOU ${pid}
|
173
|
+
sleep 2
|
174
|
+
count=$((count + 1))
|
175
|
+
done
|
176
|
+
|
177
|
+
if [ `free_mem` -lt $min_mem ] ; then
|
178
|
+
log "Still not enough memory to restart. Killing the master and allowing upstart to restart."
|
179
|
+
kill -9 ${pid}
|
180
|
+
else
|
181
|
+
# gracefully restart all current workers to free up RAM,
|
182
|
+
# then respawn master
|
183
|
+
kill -USR2 ${pid}
|
184
|
+
log "Respawn signals HUP + USR2 sent to ${COMMAND} master ${pid}"
|
185
|
+
fi
|
186
|
+
else
|
187
|
+
log "No ${COMMAND} master found. Exiting. A new one will launch when we are restarted."
|
188
|
+
fi
|
189
|
+
elif is_service_in_state "stop" ; then
|
190
|
+
local pid=`master_pid`
|
191
|
+
if [ -n "$pid" ] ; then
|
192
|
+
tries=1
|
193
|
+
while is_pid_running ${pid} && [ $tries -le 5 ] ; do
|
194
|
+
log "Service is STOPPING. Trying to kill '${COMMAND}' at pid '${pid}'. Try ${tries}"
|
195
|
+
kill ${pid}
|
196
|
+
tries=$(( $tries + 1 ))
|
197
|
+
sleep 1
|
198
|
+
done
|
199
|
+
|
200
|
+
if is_pid_running ${pid} ; then
|
201
|
+
log "Done waiting for '${COMMAND}' process '${pid}' to die. Killing for realz"
|
202
|
+
kill -9 ${pid}
|
203
|
+
else
|
204
|
+
log "${COMMAND} process '${pid}' is dead."
|
205
|
+
fi
|
206
|
+
fi
|
207
|
+
else
|
208
|
+
log "Service is neither stopping nor restarting. Exiting."
|
209
|
+
fi
|
210
|
+
else
|
211
|
+
log "Not checking for restart"
|
212
|
+
fi
|
213
|
+
}
|
214
|
+
|
215
|
+
# Upstart does not have the concept of "restart". When you restart a service it is simply
|
216
|
+
# stopped and started. But this defeats the purpose of unicorn's USR2 no downtime trick.
|
217
|
+
# So we check the service states of the foreman exported services. If any of them are
|
218
|
+
# start/stopping or start/post-stop it means that they are stopping but that the service
|
219
|
+
# itself is still schedule to run. This means restart. We can use this to differentiate between
|
220
|
+
# restarting and stopping so we can signal unicorn to restart or actually kill it appropriately.
|
221
|
+
is_service_in_state() {
|
222
|
+
local STATE=$1
|
223
|
+
if [ "$STATE" = "restart" ] ; then
|
224
|
+
PATTERN="(start/stopping|start/post-stop)"
|
225
|
+
elif [ "$STATE" = "stop" ] ; then
|
226
|
+
PATTERN="/stop"
|
227
|
+
else
|
228
|
+
log "is_service_in_state: State must be one of 'stop' or 'restart'. Got '${STATE}'"
|
229
|
+
exit 1
|
230
|
+
fi
|
231
|
+
# the service that started us and the foreman parent services, pruning off everything
|
232
|
+
# after each successive dash to find parent service
|
233
|
+
# e.g. myservice-web-1 myservice-web myservice
|
234
|
+
services=( ${SERVICE} ${SERVICE%-*} ${SERVICE%%-*} )
|
235
|
+
|
236
|
+
IN_STATE=false
|
237
|
+
|
238
|
+
for service in "${services[@]}" ; do
|
239
|
+
if /sbin/status ${service} | egrep -q "${PATTERN}" ; then
|
240
|
+
log "Service ${service} is in state '${STATE}'. - '$(/sbin/status ${service})'"
|
241
|
+
IN_STATE=true
|
242
|
+
fi
|
243
|
+
done
|
244
|
+
|
245
|
+
$IN_STATE # this is the return code for this function
|
246
|
+
}
|
247
|
+
|
248
|
+
#############################################################
|
249
|
+
##
|
250
|
+
## Trap incoming signals
|
251
|
+
##
|
252
|
+
#############################################################
|
253
|
+
|
254
|
+
# trap TERM which is what upstart uses to both stop and restart (stop/start)
|
255
|
+
trap "respawn_new_master" EXIT
|
256
|
+
|
257
|
+
#############################################################
|
258
|
+
##
|
259
|
+
## Main execution
|
260
|
+
##
|
261
|
+
#############################################################
|
262
|
+
|
263
|
+
check_environment
|
264
|
+
|
265
|
+
kill_orphaned_workers
|
266
|
+
|
267
|
+
if ! master_pid ; then
|
268
|
+
|
269
|
+
# make sure it uses the 'currrent' symlink and not the actual path
|
270
|
+
export BUNDLE_GEMFILE=${BASE_DIR}/Gemfile
|
271
|
+
|
272
|
+
log "No ${COMMAND} master found. Launching new ${COMMAND} master in env '$RACK_ENV' in directory '$BASE_DIR', BUNDLE_GEMFILE=$BUNDLE_GEMFILE"
|
273
|
+
|
274
|
+
mkdir -p "${LOG_DIR}"
|
275
|
+
|
276
|
+
# setsid to start this process in a new session because when upstart stops or restarts
|
277
|
+
# a service it kills the entire process group of the service and relaunches it. Because
|
278
|
+
# we are managing the unicorn separately from upstart it needs to be in its own
|
279
|
+
# session (group of process groups) so that it survives the original process group
|
280
|
+
setsid bundle exec ${COMMAND} -E ${RACK_ENV} -c ${CONFIG_FILE} >> ${LOG_DIR}/unicorn.log 2>&1 &
|
281
|
+
|
282
|
+
tries=1
|
283
|
+
while [ $tries -le 10 ] && ! master_pid ; do
|
284
|
+
log "Waiting for unicorn to launch master"
|
285
|
+
tries=$(( $tries + 1 ))
|
286
|
+
sleep 1
|
287
|
+
done
|
288
|
+
fi
|
289
|
+
|
290
|
+
PID=`master_pid`
|
291
|
+
|
292
|
+
if is_pid_running $PID ; then
|
293
|
+
# hang out while the unicorn process is alive. Once its gone we will exit
|
294
|
+
# this script. When upstart respawns us we will end up in the if statement above
|
295
|
+
# to relaunch a new unicorn master.
|
296
|
+
log "Found running ${COMMAND} master $PID. Awaiting its demise..."
|
297
|
+
while is_pid_running $PID ; do
|
298
|
+
sleep 5
|
299
|
+
done
|
300
|
+
log "${COMMAND} master $PID has exited."
|
301
|
+
else
|
302
|
+
log "Failed to start ${COMMAND} master. Will try again on respawn. Exiting"
|
303
|
+
fi
|
304
|
+
|
305
|
+
TRY_RESTART=false
|