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,63 @@
|
|
1
|
+
module Slugforge
|
2
|
+
module Build
|
3
|
+
class Package < Slugforge::BuildCommand
|
4
|
+
desc :package, 'package the project'
|
5
|
+
def call
|
6
|
+
existing = Dir.glob('*.slug')
|
7
|
+
|
8
|
+
if options[:clean]
|
9
|
+
existing.each do |path|
|
10
|
+
logger.say_status :clean, path, :red
|
11
|
+
File.delete(path)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
logger.say_status :execute, "fpm #{package_file_name}"
|
16
|
+
execute(fpm_command)
|
17
|
+
|
18
|
+
if options[:deploy]
|
19
|
+
invoke Slugforge::Commands::Deploy, [:file, package_file_name, options[:deploy]], []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
default_task :call
|
23
|
+
|
24
|
+
private
|
25
|
+
def fpm_command
|
26
|
+
command = ['fpm']
|
27
|
+
command << '--verbose'
|
28
|
+
command << "--package #{package_file_name}"
|
29
|
+
command << "--maintainer=#{`whoami`.chomp}"
|
30
|
+
command << "-C #{project_root}"
|
31
|
+
command << '-s dir'
|
32
|
+
command << '-t sh'
|
33
|
+
command << "-n #{project_name}"
|
34
|
+
command << "-v #{date_stamp}"
|
35
|
+
command << '--template-scripts'
|
36
|
+
command << post_install_template_variables
|
37
|
+
command << "--after-install #{post_install_script_path}"
|
38
|
+
unless options[:'with-git']
|
39
|
+
command << "--exclude '.git'"
|
40
|
+
command << "--exclude '.git/**'"
|
41
|
+
end
|
42
|
+
command << "--exclude '*.slug'"
|
43
|
+
command << "--exclude 'log/**'"
|
44
|
+
command << "--exclude 'tmp/**'"
|
45
|
+
command << "--exclude 'vendor/bundle/ruby/1.9.1/cache/*'"
|
46
|
+
command << "." # package all the things
|
47
|
+
command.join(' ')
|
48
|
+
end
|
49
|
+
|
50
|
+
def post_install_script_path
|
51
|
+
scripts_dir('post-install.sh')
|
52
|
+
end
|
53
|
+
|
54
|
+
def post_install_template_variables
|
55
|
+
variables = {
|
56
|
+
'release_id' => File.basename(package_file_name, ".*")
|
57
|
+
}.map do |key, value|
|
58
|
+
"--template-value #{key}=#{value}"
|
59
|
+
end.join(' ')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'slugforge/helper'
|
3
|
+
require 'slugforge/commands'
|
4
|
+
|
5
|
+
# Don't display warnings to the user
|
6
|
+
$VERBOSE = nil
|
7
|
+
|
8
|
+
$stdout.sync = true
|
9
|
+
|
10
|
+
module Slugforge
|
11
|
+
class Cli < Slugforge::Command
|
12
|
+
|
13
|
+
desc 'version', 'display the current version'
|
14
|
+
def version
|
15
|
+
logger.say_json :slugforge => Slugforge::VERSION
|
16
|
+
logger.say "slugforge #{Slugforge::VERSION}"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'build [ARGS]', 'build a new slug (`slugforge build` for more help)'
|
20
|
+
option :ruby, :type => :string, :aliases => '-r',
|
21
|
+
:desc => 'Ruby version this package requires'
|
22
|
+
option :path, :type => :string, :default => Dir.pwd,
|
23
|
+
:desc => 'The path to the files being packaged'
|
24
|
+
option :clean, :type => :boolean,
|
25
|
+
:desc => 'Clean existing slugs from current directory before build'
|
26
|
+
option :deploy, :type => :string,
|
27
|
+
:desc => 'Deploy the slug if the build was successful'
|
28
|
+
option :'with-git', :type => :boolean,
|
29
|
+
:desc => 'include the .git folder in the slug'
|
30
|
+
def build
|
31
|
+
verify_procfile_exists!
|
32
|
+
invoke Slugforge::Commands::Build
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'deploy <command> [ARGS]', 'deploy a slug to a host (`slugforge deploy` for more help)'
|
36
|
+
subcommand 'deploy', Slugforge::Commands::Deploy
|
37
|
+
|
38
|
+
desc 'project <command> [ARGS]', 'manage projects (`slugforge project` for more help)'
|
39
|
+
subcommand 'project', Slugforge::Commands::Project
|
40
|
+
|
41
|
+
desc 'tag <command> [ARGS]', 'manage project tags (`slugforge tag` for more help)'
|
42
|
+
subcommand 'tag', Slugforge::Commands::Tag
|
43
|
+
|
44
|
+
desc 'wrangler <command> [ARGS]', 'list, push and delete slugs (`slugforge wrangler` for more help)'
|
45
|
+
subcommand 'wrangler', Slugforge::Commands::Wrangler
|
46
|
+
|
47
|
+
# subcommand method name is configuration to not conflict with the config method on all commands
|
48
|
+
desc 'config <command> [ARGS]', 'configure slugforge (`slugforge config` for more help)'
|
49
|
+
subcommand 'configuration', Slugforge::Commands::Config
|
50
|
+
|
51
|
+
def help(command = nil, subcommand = nil)
|
52
|
+
return super if command
|
53
|
+
|
54
|
+
self.class.help(shell, subcommand)
|
55
|
+
|
56
|
+
logger.say <<-HELP
|
57
|
+
Project Naming
|
58
|
+
|
59
|
+
The easiest way to name a project is to name it after a repository. The only reason you may have to use a different
|
60
|
+
name is for testing of some kind. That said, once you've named your project there are a few ways for slugforge to
|
61
|
+
determine what project you are attempting to run project-specific commands (such as build) against. There are two
|
62
|
+
basic ways of telling slugforge which project you are working with:
|
63
|
+
|
64
|
+
1. With the provided configuration option (through a CLI flag, environment variable or config file)
|
65
|
+
2. By running the slugforge command from inside the project's repository. slugforge will use the name of the
|
66
|
+
project's root folder (as defined by the location of .git), which generally matches the name of the repository
|
67
|
+
which should be the name of the project in slugforge.
|
68
|
+
|
69
|
+
Configuring the CLI
|
70
|
+
|
71
|
+
The configuration options above can all be configured through several candidate configuration files, environment
|
72
|
+
variables or the flags as shown. Precedence is by proximity to the command: flags trump environment, which trumps
|
73
|
+
configuration files. Slugforge will attempt to load configuration files in the following locations, listed in order
|
74
|
+
of priority highest to lowest:
|
75
|
+
|
76
|
+
.slugforge
|
77
|
+
~/.slugforge
|
78
|
+
/etc/slugforge
|
79
|
+
|
80
|
+
Configuration files are written in yaml for simplicity. Below is a list of each option and the keys expected for each
|
81
|
+
type. File keys should be split on periods and expanded into hashes. AWS buckets accepts a comma seperated list,
|
82
|
+
which will be tried from first to last. There is an example config at the end of this screen.
|
83
|
+
|
84
|
+
HELP
|
85
|
+
|
86
|
+
rows = []
|
87
|
+
rows << %w(CLI Environment File)
|
88
|
+
Slugforge::Configuration.options.each do |name, config|
|
89
|
+
rows << [config[:option], config[:env], config[:key]]
|
90
|
+
|
91
|
+
unless rows.last.first.nil?
|
92
|
+
rows.last[0] = "--#{rows.last.first}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
print_table(rows, :indent => 4)
|
96
|
+
|
97
|
+
logger.say <<-HELP
|
98
|
+
|
99
|
+
Example configuration file
|
100
|
+
|
101
|
+
aws:
|
102
|
+
access_key: hashhashhashhashhash
|
103
|
+
secret_key: hashhashhashhashhashhashhashhashhashhash
|
104
|
+
|
105
|
+
HELP
|
106
|
+
end
|
107
|
+
|
108
|
+
if binding.respond_to?(:pry)
|
109
|
+
desc 'pry', 'start a pry session inside of a Thor command', :hide => true
|
110
|
+
option :path, :type => :string, :default => Dir.pwd,
|
111
|
+
:desc => 'The path to the files being packaged'
|
112
|
+
def pry
|
113
|
+
binding.pry
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
desc 'debug', 'run a test command inside slugforge', :hide => true
|
118
|
+
option :path, :type => :string, :default => Dir.pwd,
|
119
|
+
:desc => 'The path to the files being packaged'
|
120
|
+
def debug(cmd)
|
121
|
+
eval(cmd)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Slugforge
|
2
|
+
module Commands
|
3
|
+
autoload :Build, 'slugforge/commands/build'
|
4
|
+
autoload :Config, 'slugforge/commands/config'
|
5
|
+
autoload :Deploy, 'slugforge/commands/deploy'
|
6
|
+
autoload :Project, 'slugforge/commands/project'
|
7
|
+
autoload :Tag, 'slugforge/commands/tag'
|
8
|
+
autoload :Tjs, 'slugforge/commands/tjs'
|
9
|
+
autoload :Wrangler, 'slugforge/commands/wrangler'
|
10
|
+
end
|
11
|
+
|
12
|
+
class JsonError < Thor::Error
|
13
|
+
def initialize(message)
|
14
|
+
super({error: message}.to_json)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Command < Thor
|
19
|
+
include Thor::Actions
|
20
|
+
include Slugforge::Helper
|
21
|
+
|
22
|
+
check_unknown_options!
|
23
|
+
|
24
|
+
# Add Thor::Actions options
|
25
|
+
add_runtime_options!
|
26
|
+
|
27
|
+
|
28
|
+
class << self
|
29
|
+
# Parses the command and options from the given args, instantiate the class
|
30
|
+
# and invoke the command. This method is used when the arguments must be parsed
|
31
|
+
# from an array. If you are inside Ruby and want to use a Thor class, you
|
32
|
+
# can simply initialize it:
|
33
|
+
#
|
34
|
+
# script = MyScript.new(args, options, config)
|
35
|
+
# script.invoke(:command, first_arg, second_arg, third_arg)
|
36
|
+
#
|
37
|
+
def start(given_args=ARGV, config={})
|
38
|
+
# Loads enabled slugins. This must be done before the CLI is instantiated so that new commands
|
39
|
+
# will be found. Activation of slugins must be delayed until the command line options are parsed
|
40
|
+
# so that the full config will be available.
|
41
|
+
Configuration.new
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# ==== Parameters
|
47
|
+
# args<Array[Object]>:: An array of objects. The objects are applied to their
|
48
|
+
# respective accessors declared with <tt>argument</tt>.
|
49
|
+
#
|
50
|
+
# options<Hash>:: Either an array of command-line options requiring parsing or
|
51
|
+
# a hash of pre-parsed options.
|
52
|
+
#
|
53
|
+
# config<Hash>:: Configuration for this Thor class.
|
54
|
+
#
|
55
|
+
def initialize(args=[], options=[], config={})
|
56
|
+
@command_start_time = Time.now()
|
57
|
+
|
58
|
+
super
|
59
|
+
|
60
|
+
# Configuration must be
|
61
|
+
# - created after command line is parsed (so not in #start)
|
62
|
+
# - inherited from parent commands
|
63
|
+
if config[:invoked_via_subcommand]
|
64
|
+
@config = config[:shell].base.config
|
65
|
+
else
|
66
|
+
@config = Configuration.new(self.options)
|
67
|
+
@config.activate_slugins
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def config
|
74
|
+
@config
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.exit_on_failure?
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.inherited(base)
|
82
|
+
base.source_root templates_dir
|
83
|
+
end
|
84
|
+
|
85
|
+
def publish(event, *args)
|
86
|
+
ActiveSupport::Notifications.publish(event, self, *args) if notifications_enabled?
|
87
|
+
rescue => e
|
88
|
+
clean_trace = e.backtrace.reject { |l| l =~ /active_support|thor|bin\/slugforge/ } # reject parts of the stack containing active_support, thor, or bin/slugforge
|
89
|
+
logger.say_status :error, "[notification #{args.first}] #{e.message}\n" + clean_trace.join("\n"), :red
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# This class overrides #banner, forcing the subcommand parameter to be true by default. This gets around a bug in
|
94
|
+
# Thor, and causes subcommands to properly display their parent command in the help text for the subcommand.
|
95
|
+
class SubCommand < Command
|
96
|
+
def self.banner(command, namespace = nil, subcommand = true)
|
97
|
+
super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class BuildCommand < Command
|
102
|
+
class_option :ruby, :type => :string, :aliases => '-r',
|
103
|
+
:desc => 'Ruby version this package requires'
|
104
|
+
class_option :path, :type => :string, :default => Dir.pwd,
|
105
|
+
:desc => 'The path to the files being packaged'
|
106
|
+
class_option :clean, :type => :boolean,
|
107
|
+
:desc => 'Clean existing slugs from current directory before build'
|
108
|
+
class_option :deploy, :type => :string,
|
109
|
+
:desc => 'Deploy the slug if the build was successful'
|
110
|
+
class_option :'with-git', :type => :boolean,
|
111
|
+
:desc => 'include the .git folder in the slug'
|
112
|
+
|
113
|
+
def self.inherited(base)
|
114
|
+
base.source_root templates_dir
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Group < Thor::Group
|
119
|
+
include Thor::Actions
|
120
|
+
include Slugforge::Helper
|
121
|
+
|
122
|
+
# Add Thor::Actions options
|
123
|
+
add_runtime_options!
|
124
|
+
|
125
|
+
def self.inherited(base)
|
126
|
+
base.source_root templates_dir
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'slugforge/build'
|
2
|
+
|
3
|
+
module Slugforge
|
4
|
+
module Commands
|
5
|
+
class Build < Slugforge::Group
|
6
|
+
def build_project
|
7
|
+
invoke Slugforge::Build::BuildProject
|
8
|
+
end
|
9
|
+
|
10
|
+
def export_upstart
|
11
|
+
invoke Slugforge::Build::ExportUpstart
|
12
|
+
end
|
13
|
+
|
14
|
+
def package
|
15
|
+
invoke Slugforge::Build::Package
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Slugforge
|
2
|
+
module Commands
|
3
|
+
class Config < Slugforge::SubCommand
|
4
|
+
desc 'show', 'Print current configuration options'
|
5
|
+
def show
|
6
|
+
if json?
|
7
|
+
logger.say_json config.values
|
8
|
+
else
|
9
|
+
logger.say "The following configuration options are in use:"
|
10
|
+
rows = config.values.map { |name, value| [name, value] }
|
11
|
+
print_table(rows, :indent => 4)
|
12
|
+
|
13
|
+
unless config.slugins.empty?
|
14
|
+
logger.say "Slugins detected:"
|
15
|
+
rows = config.slugins.map do |(name, slugin)|
|
16
|
+
[name, slugin.spec.version, slugin.enabled ? 'enabled' : 'disabled']
|
17
|
+
end
|
18
|
+
print_table(rows, :indent => 4)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,383 @@
|
|
1
|
+
require 'slugforge/models/host'
|
2
|
+
require 'slugforge/models/host_group'
|
3
|
+
|
4
|
+
module Slugforge
|
5
|
+
module Commands
|
6
|
+
class Deploy < Slugforge::SubCommand
|
7
|
+
class_option :identity, :type => :string, :aliases => '-i',
|
8
|
+
:desc => 'The identify (.pem) file to use for authentication'
|
9
|
+
class_option :deploy_dir, :type => :string, :aliases => '-d',
|
10
|
+
:desc => 'The directory to deploy to on the server'
|
11
|
+
class_option :owner, :type => :string, :aliases => '-o',
|
12
|
+
:desc => 'Account that the application will run with when deployed'
|
13
|
+
class_option :env, :type => :string, :aliases => '-e',
|
14
|
+
:desc => 'A quoted, space-delimited list of environment variables and values'
|
15
|
+
class_option :count, :type => :numeric, :aliases => '-c',
|
16
|
+
:desc => 'Only deploy to the specified number hosts in the expanded list'
|
17
|
+
class_option :percent, :type => :numeric,
|
18
|
+
:desc => 'Only deploy to the specified percent in the expanded list'
|
19
|
+
# AWS may throttle simultaneous downloads to a file
|
20
|
+
class_option :'batch-size', :type => :numeric, :default => 85,
|
21
|
+
:desc => 'Set the number of hosts per deployment batch to help slow your roll'
|
22
|
+
class_option :'batch-count', :type => :numeric,
|
23
|
+
:desc => 'Set the number of deployment batches to help slow your roll'
|
24
|
+
class_option :'batch-pause', :type => :numeric,
|
25
|
+
:desc => 'Set the amount of time (in seconds) to pause between deployment batches, to further slow your roll'
|
26
|
+
class_option :'no-stage', :type => :boolean, :default => false,
|
27
|
+
:desc => "Don't stage the slug files on the host group members that were not targeted for the deploy"
|
28
|
+
class_option :'yes', :type => :boolean, :default => false,
|
29
|
+
:desc => "Do not prompt to proceed with deploy (a more gentle --force)"
|
30
|
+
|
31
|
+
desc 'file <filename> <hosts...> [ARGS]', 'deploy a slug file to host(s)'
|
32
|
+
option :path, :copy_type => :string, :default => Dir.pwd,
|
33
|
+
:desc => 'The path to the files being packaged'
|
34
|
+
def file(filename, *hosts)
|
35
|
+
logger.say_status :deploy, "deploying local slug #{filename}", :green
|
36
|
+
slug_name = File.basename(filename)
|
37
|
+
|
38
|
+
deploy(hosts, slug_name, deploy_options(:copy_type => :scp, :filename => filename))
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'name <name_part> <hosts...> [ARGS]', 'deploy an S3 stored slug by name to host(s) (use `wrangler list` for slug names)'
|
42
|
+
def name(name_part, *hosts)
|
43
|
+
slug = find_slug(name_part)
|
44
|
+
slug_name = File.basename(slug.key)
|
45
|
+
logger.say_status :deploy, "deploying slug #{slug_name} from s3", :green
|
46
|
+
|
47
|
+
url = expiring_url(slug)
|
48
|
+
deploy(hosts, slug_name, deploy_options(:copy_type => :aws_cmd, :url => url, :aws_session => aws_session, :s3_url => "s3://#{aws_bucket}/#{slug.key}"))
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'rollback <tag> <hosts...> [ARGS]', 'deploy the previous slug for a tag to host(s)'
|
52
|
+
def rollback(tag, *hosts)
|
53
|
+
raise error_class, "There is no project found named '#{project_name}'. Try setting the project name with --project" unless tag_manager.projects.include?(project_name)
|
54
|
+
data = tag_manager.rollback_slug_for_tag(project_name, tag)
|
55
|
+
if data.nil?
|
56
|
+
raise error_class, "could not find tag '#{tag}' for project '#{project_name}'"
|
57
|
+
else
|
58
|
+
logger.say_status :deploy, "deploying slug #{data}", :green
|
59
|
+
slug = find_slug(data)
|
60
|
+
slug_name = File.basename(data)
|
61
|
+
url = expiring_url(slug)
|
62
|
+
end
|
63
|
+
|
64
|
+
raise error_class, "could not determine URL for tag" unless url
|
65
|
+
deploy(hosts, slug_name, deploy_options(:copy_type => :aws_cmd, :url => url, :aws_session => aws_session, :s3_url => "s3://#{aws_bucket}/#{data}"))
|
66
|
+
end
|
67
|
+
|
68
|
+
desc 'tag <tag> <hosts...> [ARGS]', 'deploy a slug by tag to host(s)'
|
69
|
+
def tag(tag, *hosts)
|
70
|
+
raise error_class, "There is no project found named '#{project_name}'. Try setting the project name with --project" unless tag_manager.projects.include?(project_name)
|
71
|
+
data = tag_manager.slug_for_tag(project_name, tag)
|
72
|
+
if data.nil?
|
73
|
+
raise error_class, "could not find tag '#{tag}' for project '#{project_name}'"
|
74
|
+
else
|
75
|
+
logger.say_status :deploy, "deploying slug #{data}", :green
|
76
|
+
slug = find_slug(data)
|
77
|
+
slug_name = File.basename(data)
|
78
|
+
url = expiring_url(slug)
|
79
|
+
end
|
80
|
+
|
81
|
+
raise error_class, "could not determine URL for tag" unless url
|
82
|
+
|
83
|
+
deploy(hosts, slug_name, deploy_options(:copy_type => :aws_cmd, :url => url, :aws_session => aws_session, :s3_url => "s3://#{aws_bucket}/#{data}"))
|
84
|
+
end
|
85
|
+
|
86
|
+
desc 'ssh <hosts...> [ARGS]', 'log in to a box for testing', :hide => true
|
87
|
+
def ssh(*hosts)
|
88
|
+
deploy hosts, nil, deploy_options(:copy_type => :ssh)
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def deploy_options(opts={})
|
93
|
+
{
|
94
|
+
:username => config.ssh_username,
|
95
|
+
:deploy_dir => options[:deploy_dir] || self.deploy_dir,
|
96
|
+
:owner => options[:owner],
|
97
|
+
:identity => options[:identity],
|
98
|
+
:env => options[:env],
|
99
|
+
:force => force?,
|
100
|
+
:pretend => pretend?
|
101
|
+
}.merge(opts)
|
102
|
+
end
|
103
|
+
|
104
|
+
def deploy(host_patterns, slug_name, deploy_opts)
|
105
|
+
host_groups = determine_host_groups(host_patterns)
|
106
|
+
return unless confirm_deployment_start?(host_groups)
|
107
|
+
|
108
|
+
# Stage file on all other hosts in facets, unless told otherwise
|
109
|
+
unless options[:'no-stage']
|
110
|
+
host_groups.each do |group|
|
111
|
+
group.hosts.each { |host| host.add_action(:stage) unless host.install? }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
logger.say_status :deploy, "beginning deployment", :green
|
116
|
+
|
117
|
+
# Organize the list of hosts to more evenly spread load across impacted facets
|
118
|
+
hosts = order_deploy(unique_hosts(host_groups))
|
119
|
+
batches = (batch_size(hosts.count) == 0) ? [hosts] : hosts.each_slice(batch_size(hosts.count)).to_a
|
120
|
+
|
121
|
+
partial_deploy_type = [:count, :percent].detect{ |s| options[s] }
|
122
|
+
|
123
|
+
publish('deploy.started', {
|
124
|
+
:partial_deploy_method => partial_deploy_type,
|
125
|
+
:partial_deploy_limit => options[partial_deploy_type],
|
126
|
+
:host_groups => host_groups,
|
127
|
+
:batch_size => batch_size(hosts.count),
|
128
|
+
:project => project_name,
|
129
|
+
:slug_name => slug_name
|
130
|
+
})
|
131
|
+
|
132
|
+
deploy_in_batches(batches, slug_name, deploy_opts)
|
133
|
+
|
134
|
+
logger.say_status :deploy, "deployment complete!", :green
|
135
|
+
say_deploy_status(host_groups, slug_name)
|
136
|
+
|
137
|
+
|
138
|
+
publish('deploy.finished', {
|
139
|
+
:success => hosts.all?(&:success?),
|
140
|
+
:partial_deploy_method => partial_deploy_type,
|
141
|
+
:partial_deploy_limit => options[:count] || options[:percent],
|
142
|
+
:host_groups => host_groups,
|
143
|
+
:batch_size => batch_size(hosts.count),
|
144
|
+
:project => project_name,
|
145
|
+
:slug_name => slug_name
|
146
|
+
})
|
147
|
+
|
148
|
+
host_groups
|
149
|
+
end
|
150
|
+
|
151
|
+
def deploy_in_batches(batches, slug_name, deploy_opts)
|
152
|
+
pause = options[:'batch-pause'].to_i
|
153
|
+
batches.each.with_index(1) do |batch, i|
|
154
|
+
logger.say "deploying batch #{i} of #{batches.count}", :magenta if batches.count > 1
|
155
|
+
threads = {}
|
156
|
+
batch.each do |host|
|
157
|
+
thread = Thread.new do
|
158
|
+
host.deploy(slug_name, logger, deploy_opts)
|
159
|
+
end
|
160
|
+
threads[host.ip] = thread
|
161
|
+
end
|
162
|
+
join_batch_threads(threads, batch, logger)
|
163
|
+
unless (batches.length == i || pause == 0)
|
164
|
+
logger.say "batch #{i} complete; pausing for #{pause} seconds", :magenta
|
165
|
+
sleep pause
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def say_deploy_status(host_groups, slug_name)
|
171
|
+
return nil if host_groups.nil?
|
172
|
+
hosts = unique_hosts(host_groups)
|
173
|
+
return nil if hosts.empty?
|
174
|
+
total_count = hosts.count
|
175
|
+
successful = hosts.select { |h| h.success? }.count
|
176
|
+
overall_success = (total_count == successful)
|
177
|
+
|
178
|
+
if json?
|
179
|
+
logger.say_json :hosts => hosts.map(&:to_status), :success => overall_success
|
180
|
+
else
|
181
|
+
status_color = overall_success ? :green : :red
|
182
|
+
logger.say "\n#{'-'*22}\n| Deployment Summary |\n#{'-'*22}", status_color
|
183
|
+
logger.say "Deployed #{slug_name} to "
|
184
|
+
logger.say "#{successful} ", status_color
|
185
|
+
logger.say "of "
|
186
|
+
logger.say "#{total_count} ", status_color
|
187
|
+
logger.say "hosts in "
|
188
|
+
logger.say "#{elapsed_time}", :yellow
|
189
|
+
|
190
|
+
unless overall_success
|
191
|
+
indent = Math.log10(total_count - successful).round + 4
|
192
|
+
logger.say "\nFailures:", :red
|
193
|
+
count = 0
|
194
|
+
hosts.each do |host|
|
195
|
+
unless host.success?
|
196
|
+
logger.say ""
|
197
|
+
logger.say "%#{indent}s" % "#{count += 1}) ", :red
|
198
|
+
logger.say "#{host.name}", :red
|
199
|
+
print_wrapped host.output.join("\n"), :indent => indent
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
log_rollout_status(host_groups)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def determine_host_groups(host_patterns)
|
208
|
+
say_option_status host_patterns
|
209
|
+
logger.say_status :deploy, "determining deployment targets", :green
|
210
|
+
host_groups = partial_install_groups(host_groups_for_patterns(host_patterns))
|
211
|
+
end
|
212
|
+
|
213
|
+
def confirm_deployment_start?(host_groups)
|
214
|
+
say_predeploy_status(host_groups)
|
215
|
+
if !(force? || json? || options[:yes]) && (ask("Are you sure you wish to deploy? [yN]").downcase != 'y')
|
216
|
+
logger.say_status :deploy, "deployment aborted!", :red
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
# Reset the start time for more useful reporting
|
220
|
+
@command_start_time = Time.now()
|
221
|
+
true
|
222
|
+
end
|
223
|
+
|
224
|
+
def unique_hosts(host_groups)
|
225
|
+
# If we're not staging the slug, return just the hosts being installed to
|
226
|
+
options[:'no-stage'] ? host_groups.collect {|host_group| host_group.hosts_for_action(:install)}.flatten.uniq { |h| h.name } : host_groups.map(&:hosts).flatten.uniq { |h| h.name }
|
227
|
+
end
|
228
|
+
|
229
|
+
def order_deploy(hosts)
|
230
|
+
# Percolate installations to the top, then stripe across batches
|
231
|
+
hosts.sort! {|a,b| a.install? ? -1 : 1}
|
232
|
+
results=[]
|
233
|
+
batches = hosts.count / batch_size(hosts.count)
|
234
|
+
hosts.each.with_index {|item, index| results[index % batches].nil? ? results[index % batches] = [item] : results[index % batches] << item }
|
235
|
+
results.flatten
|
236
|
+
end
|
237
|
+
|
238
|
+
def join_batch_threads(threads, hosts, logger)
|
239
|
+
joined = false
|
240
|
+
while !joined
|
241
|
+
begin
|
242
|
+
threads.map { |ip,thread| thread }.map(&:join)
|
243
|
+
joined = true
|
244
|
+
rescue Interrupt # Ctrl+C
|
245
|
+
logger.say "\nWe are #{elapsed_time} in. Stragglers for this batch:", :magenta
|
246
|
+
hosts.reject { |host| host.complete? }.each_with_index do |host, stripe|
|
247
|
+
logger.say " #{host.name} (Timeline: #{host.timeline})", stripe.odd? ? :cyan : :yellow
|
248
|
+
end
|
249
|
+
logger.say "Maybe you should give 'em them the clamps?", :magenta
|
250
|
+
case ask("(T)erminate stragglers, (F)ail stragglers, (?) for help, or anything else to keep waiting:").downcase
|
251
|
+
when 'f'
|
252
|
+
break
|
253
|
+
when 't'
|
254
|
+
logger.say "Gee, you think? You think that maybe I should use these clamps that I use every day at every opportunity? You're a freakin' genius, you idiot!", :magenta
|
255
|
+
hosts.each_with_index do |host, index|
|
256
|
+
next if host.complete?
|
257
|
+
if host.id.nil? || !host.is_autoscaled?
|
258
|
+
logger.say "Can't terminate #{host.name} as it is not part of an autoscaler"
|
259
|
+
else
|
260
|
+
logger.say "Terminating #{host.name} (#{['Clamp.','Clamp!','Clampity Clamp!'][index%3]})", index.odd? ? :cyan : :yellow
|
261
|
+
threads[host.ip].terminate
|
262
|
+
host.record_event(:terminated)
|
263
|
+
autoscaling.terminate_instance_in_auto_scaling_group(host.id, false)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
when '?'
|
267
|
+
straggler_help
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def straggler_help
|
274
|
+
logger.say <<-EOF
|
275
|
+
|
276
|
+
T) Attempt to terminate the stragglers and let their autoscaling group create new instances. The deploy will end if everyone who had not completed could be terminated.
|
277
|
+
F) Mark the remaining stragglers are failed and end the deployment
|
278
|
+
?) Display this help and resume the deploy
|
279
|
+
EOF
|
280
|
+
end
|
281
|
+
|
282
|
+
def say_option_status(host_patterns)
|
283
|
+
subset_name = if options[:count]
|
284
|
+
"#{options[:count]} server#{(options[:count] == 1) ? '' : 's'}"
|
285
|
+
elsif options[:percent]
|
286
|
+
"#{options[:percent]}% of servers"
|
287
|
+
else
|
288
|
+
"all servers"
|
289
|
+
end
|
290
|
+
logger.say_status :deploy, "targeting #{subset_name} for: #{host_patterns.join(', ')}", :green
|
291
|
+
end
|
292
|
+
|
293
|
+
def say_predeploy_status(host_groups)
|
294
|
+
total_count = host_groups.inject(0) { |sum, host_group| sum += host_group.hosts.count }
|
295
|
+
install_count = 0
|
296
|
+
host_groups.each_with_index do |host_group, stripe|
|
297
|
+
raise error_class, "Host group #{host_group.name} was empty!" if host_group.hosts.nil?
|
298
|
+
install_hosts = host_group.hosts_for_action(:install)
|
299
|
+
install_count += install_hosts.count
|
300
|
+
install_hosts.each do |host|
|
301
|
+
unless json?
|
302
|
+
logger.say # Add a newline for cleaner paste into Flowdock
|
303
|
+
logger.say "#{host_group.name}: ", stripe.odd? ? :cyan : :yellow
|
304
|
+
logger.say "#{host.name}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
logger.say # Add a newline for cleaner paste into Flowdock
|
309
|
+
logger.say_status :deploy, "#{with_units(install_count, 'host')} targeted for installation out of #{with_units(total_count, 'host')} total", :green
|
310
|
+
|
311
|
+
batch_host_count = options[:'no-stage'] ? host_groups.inject(0) { |sum, host_group| sum += host_group.hosts_for_action(:install).count } : total_count
|
312
|
+
batches = (batch_host_count/batch_size(batch_host_count).to_f).ceil
|
313
|
+
logger.say_status :deploy, "using #{batches} batch#{batches == 1 ? '' : 'es'} of #{with_units(batch_size(batch_host_count), 'host')} for installation #{options[:'no-stage'] ? '' : 'and staging'} ", :green
|
314
|
+
end
|
315
|
+
|
316
|
+
def with_units(value, unit)
|
317
|
+
"#{value} #{unit}#{(value == 1) ? '' : 's'}"
|
318
|
+
end
|
319
|
+
|
320
|
+
def log_rollout_status(host_groups)
|
321
|
+
return if host_groups.nil?
|
322
|
+
result = { :environment => overall_status, :hostgroups => [] }
|
323
|
+
host_groups.each do |host_group|
|
324
|
+
host_group.hosts.each do |host|
|
325
|
+
result[:hostgroups] << { :group => host_group.name }.merge(host.to_status)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
filename = "slugforge_status-#{date_stamp}.json"
|
329
|
+
logger.say "Writing full status report to #{filename}"
|
330
|
+
File.open(filename, "w") do |f|
|
331
|
+
f.write(JSON.pretty_generate(result))
|
332
|
+
end
|
333
|
+
purge_old_files 'slugforge_status-*.json'
|
334
|
+
end
|
335
|
+
|
336
|
+
def overall_status
|
337
|
+
{
|
338
|
+
:command_line => "#{$0} #{$*.join(' ')}",
|
339
|
+
:options => @options,
|
340
|
+
:ruby_version => RUBY_VERSION,
|
341
|
+
:ec2_access_key => @ec2_access_key,
|
342
|
+
:s3_access_key => @s3_access_key,
|
343
|
+
:git_info => git_info,
|
344
|
+
}
|
345
|
+
end
|
346
|
+
|
347
|
+
def purge_old_files(file_mask, keep_count = 10)
|
348
|
+
old_files = Dir.glob(file_mask).sort_by{ |f| File.ctime(f) }.reverse.slice(keep_count..-1)
|
349
|
+
File.delete(*old_files)
|
350
|
+
end
|
351
|
+
|
352
|
+
def host_groups_for_patterns(host_patterns)
|
353
|
+
host_groups = HostGroup.discover(host_patterns, compute)
|
354
|
+
raise error_class, "Unable to determine what host or group of hosts you meant with '#{pattern}'." unless host_groups
|
355
|
+
# determine unique hosts in each list, then sort by IP (alphabetically) to make partial deploys essentially deterministic
|
356
|
+
host_groups
|
357
|
+
end
|
358
|
+
|
359
|
+
def partial_install_groups(host_groups)
|
360
|
+
if options[:percent]
|
361
|
+
return host_groups.each { |host_group| host_group.install_percent_of_hosts(options[:percent]) }
|
362
|
+
elsif options[:count]
|
363
|
+
return host_groups.each { |host_group| host_group.install_number_of_hosts(options[:count]) }
|
364
|
+
end
|
365
|
+
host_groups.each { |host_group| host_group.install_all }
|
366
|
+
end
|
367
|
+
|
368
|
+
private
|
369
|
+
def batch_size(host_count = 1)
|
370
|
+
if options[:'batch-count'] && host_count >= options[:'batch-count'].to_i
|
371
|
+
batch_count = options[:'batch-count'] < 1 ? 1 : options[:'batch-count']
|
372
|
+
(host_count / batch_count.to_f).ceil
|
373
|
+
elsif options[:'batch-size'] && host_count > options[:'batch-size'].to_i
|
374
|
+
batch_size = options[:'batch-size'].to_i
|
375
|
+
batch_size = batch_size < 1 ? host_count : batch_size
|
376
|
+
else
|
377
|
+
host_count > 1 ? host_count : 1
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|