crazy-yard 3.2.2
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/LICENSE +19 -0
- data/README.md +438 -0
- data/bin/ey +9 -0
- data/lib/engineyard.rb +9 -0
- data/lib/engineyard/cli.rb +816 -0
- data/lib/engineyard/cli/api.rb +98 -0
- data/lib/engineyard/cli/recipes.rb +129 -0
- data/lib/engineyard/cli/ui.rb +275 -0
- data/lib/engineyard/cli/web.rb +85 -0
- data/lib/engineyard/config.rb +158 -0
- data/lib/engineyard/deploy_config.rb +65 -0
- data/lib/engineyard/deploy_config/ref.rb +56 -0
- data/lib/engineyard/error.rb +82 -0
- data/lib/engineyard/eyrc.rb +59 -0
- data/lib/engineyard/repo.rb +105 -0
- data/lib/engineyard/serverside_runner.rb +159 -0
- data/lib/engineyard/templates.rb +6 -0
- data/lib/engineyard/templates/ey.yml.erb +196 -0
- data/lib/engineyard/templates/ey_yml.rb +119 -0
- data/lib/engineyard/thor.rb +215 -0
- data/lib/engineyard/version.rb +4 -0
- data/lib/vendor/thor/Gemfile +15 -0
- data/lib/vendor/thor/LICENSE.md +20 -0
- data/lib/vendor/thor/README.md +35 -0
- data/lib/vendor/thor/lib/thor.rb +473 -0
- data/lib/vendor/thor/lib/thor/actions.rb +318 -0
- data/lib/vendor/thor/lib/thor/actions/create_file.rb +105 -0
- data/lib/vendor/thor/lib/thor/actions/create_link.rb +60 -0
- data/lib/vendor/thor/lib/thor/actions/directory.rb +119 -0
- data/lib/vendor/thor/lib/thor/actions/empty_directory.rb +137 -0
- data/lib/vendor/thor/lib/thor/actions/file_manipulation.rb +314 -0
- data/lib/vendor/thor/lib/thor/actions/inject_into_file.rb +109 -0
- data/lib/vendor/thor/lib/thor/base.rb +652 -0
- data/lib/vendor/thor/lib/thor/command.rb +136 -0
- data/lib/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +80 -0
- data/lib/vendor/thor/lib/thor/core_ext/io_binary_read.rb +12 -0
- data/lib/vendor/thor/lib/thor/core_ext/ordered_hash.rb +100 -0
- data/lib/vendor/thor/lib/thor/error.rb +28 -0
- data/lib/vendor/thor/lib/thor/group.rb +282 -0
- data/lib/vendor/thor/lib/thor/invocation.rb +172 -0
- data/lib/vendor/thor/lib/thor/parser.rb +4 -0
- data/lib/vendor/thor/lib/thor/parser/argument.rb +74 -0
- data/lib/vendor/thor/lib/thor/parser/arguments.rb +171 -0
- data/lib/vendor/thor/lib/thor/parser/option.rb +121 -0
- data/lib/vendor/thor/lib/thor/parser/options.rb +218 -0
- data/lib/vendor/thor/lib/thor/rake_compat.rb +72 -0
- data/lib/vendor/thor/lib/thor/runner.rb +322 -0
- data/lib/vendor/thor/lib/thor/shell.rb +88 -0
- data/lib/vendor/thor/lib/thor/shell/basic.rb +393 -0
- data/lib/vendor/thor/lib/thor/shell/color.rb +148 -0
- data/lib/vendor/thor/lib/thor/shell/html.rb +127 -0
- data/lib/vendor/thor/lib/thor/util.rb +270 -0
- data/lib/vendor/thor/lib/thor/version.rb +3 -0
- data/lib/vendor/thor/thor.gemspec +24 -0
- data/spec/engineyard/cli/api_spec.rb +50 -0
- data/spec/engineyard/cli_spec.rb +28 -0
- data/spec/engineyard/config_spec.rb +61 -0
- data/spec/engineyard/deploy_config_spec.rb +194 -0
- data/spec/engineyard/eyrc_spec.rb +76 -0
- data/spec/engineyard/repo_spec.rb +83 -0
- data/spec/engineyard_spec.rb +7 -0
- data/spec/ey/console_spec.rb +57 -0
- data/spec/ey/deploy_spec.rb +435 -0
- data/spec/ey/ey_spec.rb +23 -0
- data/spec/ey/init_spec.rb +123 -0
- data/spec/ey/list_environments_spec.rb +120 -0
- data/spec/ey/login_spec.rb +33 -0
- data/spec/ey/logout_spec.rb +24 -0
- data/spec/ey/logs_spec.rb +36 -0
- data/spec/ey/rebuild_spec.rb +18 -0
- data/spec/ey/recipes/apply_spec.rb +29 -0
- data/spec/ey/recipes/download_spec.rb +43 -0
- data/spec/ey/recipes/upload_spec.rb +99 -0
- data/spec/ey/rollback_spec.rb +73 -0
- data/spec/ey/scp_spec.rb +176 -0
- data/spec/ey/servers_spec.rb +209 -0
- data/spec/ey/ssh_spec.rb +273 -0
- data/spec/ey/status_spec.rb +45 -0
- data/spec/ey/timeout_deploy_spec.rb +18 -0
- data/spec/ey/web/disable_spec.rb +21 -0
- data/spec/ey/web/enable_spec.rb +26 -0
- data/spec/ey/web/restart_spec.rb +21 -0
- data/spec/ey/whoami_spec.rb +30 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/support/bundled_ey +7 -0
- data/spec/support/fixture_recipes.tgz +0 -0
- data/spec/support/git_repos.rb +115 -0
- data/spec/support/helpers.rb +330 -0
- data/spec/support/matchers.rb +16 -0
- data/spec/support/ruby_ext.rb +13 -0
- data/spec/support/shared_behavior.rb +278 -0
- metadata +411 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'highline'
|
|
2
|
+
require 'engineyard-cloud-client'
|
|
3
|
+
require 'engineyard/eyrc'
|
|
4
|
+
|
|
5
|
+
module EY
|
|
6
|
+
class CLI
|
|
7
|
+
class API
|
|
8
|
+
USER_AGENT = "EngineYard/#{EY::VERSION}"
|
|
9
|
+
|
|
10
|
+
attr_reader :token
|
|
11
|
+
|
|
12
|
+
def initialize(endpoint, ui, token = nil)
|
|
13
|
+
@client = EY::CloudClient.new(endpoint: endpoint, output: ui.out, user_agent: USER_AGENT)
|
|
14
|
+
@ui = ui
|
|
15
|
+
@eyrc = EY::EYRC.load
|
|
16
|
+
token_from('--api-token') { token } ||
|
|
17
|
+
token_from('$ENGINEYARD_API_TOKEN') { ENV['ENGINEYARD_API_TOKEN'] } ||
|
|
18
|
+
token_from(@eyrc.path, false) { @eyrc.api_token } ||
|
|
19
|
+
authenticate ||
|
|
20
|
+
token_not_loaded
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def respond_to?(*a)
|
|
24
|
+
super or @client.respond_to?(*a)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def method_missing(meth, *args, &block)
|
|
30
|
+
if @client.respond_to?(meth)
|
|
31
|
+
with_reauthentication { @client.send(meth, *args, &block) }
|
|
32
|
+
else
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_reauthentication
|
|
38
|
+
begin
|
|
39
|
+
yield
|
|
40
|
+
rescue EY::CloudClient::InvalidCredentials
|
|
41
|
+
if @specified || !@ui.interactive?
|
|
42
|
+
# If the token is specified, we raise immediately if it is rejected.
|
|
43
|
+
raise EY::Error, "Authentication failed: Invalid #{@source}."
|
|
44
|
+
else
|
|
45
|
+
@ui.warn "Authentication failed: Invalid #{@source}."
|
|
46
|
+
authenticate
|
|
47
|
+
retry
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the token from the provided block, saving it if it works.
|
|
53
|
+
# Specified will help us know what to do if loading the token fails.
|
|
54
|
+
# Returns true if it gets a token.
|
|
55
|
+
# Returns false if there is no token.
|
|
56
|
+
def token_from(source, specified = true)
|
|
57
|
+
token = yield
|
|
58
|
+
if token
|
|
59
|
+
@client.token = token
|
|
60
|
+
@specified = specified
|
|
61
|
+
@source = "token from #{source}"
|
|
62
|
+
@token = token
|
|
63
|
+
true
|
|
64
|
+
else
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Load the token from EY Cloud if interactive and
|
|
70
|
+
# token wasn't explicitly specified previously.
|
|
71
|
+
def authenticate
|
|
72
|
+
if @specified
|
|
73
|
+
return false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@source = "credentials"
|
|
77
|
+
@specified = false
|
|
78
|
+
|
|
79
|
+
@ui.info "We need to fetch your API token; please log in."
|
|
80
|
+
begin
|
|
81
|
+
email = @ui.ask("Email: ")
|
|
82
|
+
passwd = @ui.ask("Password: ", true)
|
|
83
|
+
@token = @client.authenticate!(email, passwd)
|
|
84
|
+
@eyrc.api_token = @token
|
|
85
|
+
true
|
|
86
|
+
rescue EY::CloudClient::InvalidCredentials
|
|
87
|
+
@ui.warn "Authentication failed. Please try again."
|
|
88
|
+
retry
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Occurs when all avenues for getting the token are exhausted.
|
|
93
|
+
def token_not_loaded
|
|
94
|
+
raise EY::Error, "Sorry, we couldn't get your API token."
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
|
|
3
|
+
module EY
|
|
4
|
+
class CLI
|
|
5
|
+
class Recipes < EY::Thor
|
|
6
|
+
desc "apply [--environment ENVIRONMENT]",
|
|
7
|
+
"Run chef recipes uploaded by '#{banner_base} recipes upload' on the specified environment."
|
|
8
|
+
long_desc <<-DESC
|
|
9
|
+
This is similar to '#{banner_base} rebuild' except Engine Yard's main
|
|
10
|
+
configuration step is skipped.
|
|
11
|
+
|
|
12
|
+
The cookbook uploaded by the '#{banner_base} recipes upload' command will be run when
|
|
13
|
+
you run '#{banner_base} recipes apply'.
|
|
14
|
+
DESC
|
|
15
|
+
|
|
16
|
+
method_option :environment, type: :string, aliases: %w(-e),
|
|
17
|
+
required: true, default: '',
|
|
18
|
+
desc: "Environment in which to apply recipes"
|
|
19
|
+
method_option :account, type: :string, aliases: %w(-c),
|
|
20
|
+
required: true, default: '',
|
|
21
|
+
desc: "Name of the account in which the environment can be found"
|
|
22
|
+
def apply
|
|
23
|
+
environment = fetch_environment(options[:environment], options[:account])
|
|
24
|
+
apply_recipes(environment)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "upload [--environment ENVIRONMENT]",
|
|
28
|
+
"Upload custom chef recipes to specified environment so they can be applied."
|
|
29
|
+
long_desc <<-DESC
|
|
30
|
+
Make an archive of the "cookbooks/" subdirectory in your current working
|
|
31
|
+
directory and upload it to Engine Yard Cloud's recipe storage.
|
|
32
|
+
|
|
33
|
+
Alternatively, specify a .tgz of a cookbooks/ directory yourself as follows:
|
|
34
|
+
|
|
35
|
+
$ #{banner_base} recipes upload -f path/to/recipes.tgz
|
|
36
|
+
|
|
37
|
+
The uploaded cookbooks will be run when executing '#{banner_base} recipes apply'
|
|
38
|
+
and also automatically each time you update/rebuild your instances.
|
|
39
|
+
DESC
|
|
40
|
+
|
|
41
|
+
method_option :environment, type: :string, aliases: %w(-e),
|
|
42
|
+
required: true, default: '',
|
|
43
|
+
desc: "Environment that will receive the recipes"
|
|
44
|
+
method_option :account, type: :string, aliases: %w(-c),
|
|
45
|
+
required: true, default: '',
|
|
46
|
+
desc: "Name of the account in which the environment can be found"
|
|
47
|
+
method_option :apply, type: :boolean,
|
|
48
|
+
desc: "Apply the recipes immediately after they are uploaded"
|
|
49
|
+
method_option :file, type: :string, aliases: %w(-f),
|
|
50
|
+
required: true, default: '',
|
|
51
|
+
desc: "Specify a gzipped tar file (.tgz) for upload instead of cookbooks/ directory"
|
|
52
|
+
def upload
|
|
53
|
+
environment = fetch_environment(options[:environment], options[:account])
|
|
54
|
+
upload_recipes(environment, options[:file])
|
|
55
|
+
if options[:apply]
|
|
56
|
+
apply_recipes(environment)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
no_tasks do
|
|
61
|
+
def apply_recipes(environment)
|
|
62
|
+
environment.run_custom_recipes
|
|
63
|
+
ui.info "Uploaded recipes started for #{environment.name}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def upload_recipes(environment, filename)
|
|
67
|
+
if filename && filename != ''
|
|
68
|
+
environment.upload_recipes_at_path(filename)
|
|
69
|
+
ui.info "Recipes file #{filename} uploaded successfully for #{environment.name}"
|
|
70
|
+
else
|
|
71
|
+
path = cookbooks_dir_archive_path
|
|
72
|
+
environment.upload_recipes_at_path(path)
|
|
73
|
+
ui.info "Recipes in cookbooks/ uploaded successfully for #{environment.name}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cookbooks_dir_archive_path
|
|
78
|
+
unless FileTest.exist?("cookbooks")
|
|
79
|
+
raise EY::Error, "Could not find chef recipes. Please run from the root of your recipes repo."
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
recipes_file = Tempfile.new("recipes")
|
|
83
|
+
|
|
84
|
+
cmd = "tar czf '#{recipes_file.path}' cookbooks/"
|
|
85
|
+
if FileTest.exist?("data_bags")
|
|
86
|
+
cmd = cmd + " data_bags/"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless system(cmd)
|
|
90
|
+
raise EY::Error, "Could not archive recipes.\nCommand `#{cmd}` exited with an error."
|
|
91
|
+
end
|
|
92
|
+
recipes_file.path
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
desc "download [--environment ENVIRONMENT]",
|
|
97
|
+
"Download a copy of the custom chef recipes from this environment into the current directory."
|
|
98
|
+
long_desc <<-DESC
|
|
99
|
+
The recipes will be unpacked into a directory called "cookbooks" in the
|
|
100
|
+
current directory. This is the opposite of 'recipes upload'.
|
|
101
|
+
|
|
102
|
+
If the cookbooks directory already exists, an error will be raised.
|
|
103
|
+
DESC
|
|
104
|
+
method_option :environment, type: :string, aliases: %w(-e),
|
|
105
|
+
required: true, default: '',
|
|
106
|
+
desc: "Environment for which to download the recipes"
|
|
107
|
+
method_option :account, type: :string, aliases: %w(-c),
|
|
108
|
+
required: true, default: '',
|
|
109
|
+
desc: "Name of the account in which the environment can be found"
|
|
110
|
+
def download
|
|
111
|
+
if File.exist?('cookbooks')
|
|
112
|
+
raise EY::Error, "Cannot download recipes, cookbooks directory already exists."
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
environment = fetch_environment(options[:environment], options[:account])
|
|
116
|
+
|
|
117
|
+
recipes = environment.download_recipes
|
|
118
|
+
cmd = "tar xzf '#{recipes.path}' cookbooks"
|
|
119
|
+
|
|
120
|
+
if system(cmd)
|
|
121
|
+
ui.info "Recipes downloaded successfully for #{environment.name}"
|
|
122
|
+
else
|
|
123
|
+
raise EY::Error, "Could not unarchive recipes.\nCommand `#{cmd}` exited with an error."
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
require 'highline'
|
|
2
|
+
|
|
3
|
+
module EY
|
|
4
|
+
class CLI
|
|
5
|
+
class UI < Thor::Base.shell
|
|
6
|
+
|
|
7
|
+
class Tee
|
|
8
|
+
def initialize(*ios)
|
|
9
|
+
@ios = ios
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def <<(str)
|
|
13
|
+
@ios.each { |io| io << str }
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Prompter
|
|
19
|
+
def self.add_answer(arg)
|
|
20
|
+
@answers ||= []
|
|
21
|
+
@answers << arg
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.questions
|
|
25
|
+
@questions
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.enable_mock!
|
|
29
|
+
@questions = []
|
|
30
|
+
@answers = []
|
|
31
|
+
@mock = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.highline
|
|
35
|
+
@highline ||= HighLine.new($stdin)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.interactive?
|
|
39
|
+
@mock || ($stdout && $stdout.tty?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.ask(question, password = false, default = nil)
|
|
43
|
+
if @mock
|
|
44
|
+
@questions ||= []
|
|
45
|
+
@questions << question
|
|
46
|
+
answer = @answers.shift
|
|
47
|
+
(answer == '' && default) ? default : answer
|
|
48
|
+
else
|
|
49
|
+
timeout_if_not_interactive do
|
|
50
|
+
highline.ask(question) do |q|
|
|
51
|
+
q.echo = "*" if password
|
|
52
|
+
q.default = default if default
|
|
53
|
+
end.to_s
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.agree(question, default)
|
|
59
|
+
if @mock
|
|
60
|
+
@questions ||= []
|
|
61
|
+
@questions << question
|
|
62
|
+
answer = @answers.shift
|
|
63
|
+
answer == '' ? default : %w[y yes].include?(answer)
|
|
64
|
+
else
|
|
65
|
+
timeout_if_not_interactive do
|
|
66
|
+
answer = highline.agree(question) {|q| q.default = default ? 'Y/n' : 'N/y' }
|
|
67
|
+
case answer
|
|
68
|
+
when 'Y/n' then true
|
|
69
|
+
when 'N/y' then false
|
|
70
|
+
else answer
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.timeout_if_not_interactive(&block)
|
|
77
|
+
if interactive?
|
|
78
|
+
block.call
|
|
79
|
+
else
|
|
80
|
+
Timeout.timeout(2, &block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def error(name, message = nil)
|
|
86
|
+
$stdout = $stderr
|
|
87
|
+
say_with_status(name, message, :red)
|
|
88
|
+
ensure
|
|
89
|
+
$stdout = STDOUT
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def warn(name, message = nil)
|
|
93
|
+
say_with_status(name, message, :yellow)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def info(message, color = nil)
|
|
97
|
+
return if quiet?
|
|
98
|
+
say_with_status(message, nil, color)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def debug(name, message = nil)
|
|
102
|
+
if ENV["DEBUG"]
|
|
103
|
+
name = name.inspect unless name.nil? or name.is_a?(String)
|
|
104
|
+
message = message.inspect unless message.nil? or message.is_a?(String)
|
|
105
|
+
say_with_status(name, message, :blue)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def say_with_status(name, message=nil, color=nil)
|
|
110
|
+
if message
|
|
111
|
+
say_status name, message, color
|
|
112
|
+
elsif name
|
|
113
|
+
say name, color
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def interactive?
|
|
118
|
+
Prompter.interactive?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def agree(message, default)
|
|
122
|
+
Prompter.agree(message, default)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def ask(message, password = false, default = nil)
|
|
126
|
+
Prompter.ask(message, password, default)
|
|
127
|
+
rescue EOFError
|
|
128
|
+
return ''
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def mute_if(bool, &block)
|
|
132
|
+
bool ? mute(&block) : yield
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def server_tuples(servers, username=nil)
|
|
136
|
+
user = username && "#{username}@"
|
|
137
|
+
|
|
138
|
+
servers.map do |server|
|
|
139
|
+
host = "#{user}#{server.hostname}"
|
|
140
|
+
[host, server.amazon_id, server.role, server.name]
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
private :server_tuples
|
|
144
|
+
|
|
145
|
+
def print_hostnames(servers, username=nil)
|
|
146
|
+
server_tuples(servers, username).each do |server_tuple|
|
|
147
|
+
puts server_tuple.first
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def print_simple_servers(servers, username=nil)
|
|
152
|
+
server_tuples(servers, username).each do |server_tuple|
|
|
153
|
+
puts server_tuple.join("\t")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def print_servers(servers, name, username=nil)
|
|
158
|
+
tuples = server_tuples(servers, username)
|
|
159
|
+
count = tuples.size
|
|
160
|
+
puts "# #{count} server#{count == 1 ? '' : 's'} on #{name}"
|
|
161
|
+
|
|
162
|
+
host_width = tuples.map {|s| s[0].length }.max
|
|
163
|
+
host_format = "%-#{host_width}s" # "%-10s" left align
|
|
164
|
+
|
|
165
|
+
role_width = tuples.map {|s| s[2].length }.max
|
|
166
|
+
role_format = "%-#{role_width}s" # "%-10s" left align
|
|
167
|
+
|
|
168
|
+
tuples.each do |server_tuple|
|
|
169
|
+
puts "#{host_format}\t%s\t#{role_format}\t%s" % server_tuple
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def print_simple_envs(envs)
|
|
174
|
+
puts envs.map{|env| env.name }.uniq.sort
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_envs(apps, default_env_name = nil)
|
|
178
|
+
apps.sort_by {|app| "#{app.account.name}/#{app.name}" }.each do |app|
|
|
179
|
+
puts "#{app.account.name}/#{app.name}"
|
|
180
|
+
if app.environments.any?
|
|
181
|
+
app.environments.sort_by {|env| env.name }.each do |env|
|
|
182
|
+
icount = env.instances_count
|
|
183
|
+
iname = case icount
|
|
184
|
+
when 0 then "(stopped)"
|
|
185
|
+
when 1 then "1 instance"
|
|
186
|
+
else "#{icount} instances"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
name = env.name == default_env_name ? "#{env.name} (default)" : env.name
|
|
190
|
+
framework_env = env.framework_env && "[#{env.framework_env.center(12)}]"
|
|
191
|
+
|
|
192
|
+
puts " #{name.ljust(30)} #{framework_env} #{iname}"
|
|
193
|
+
end
|
|
194
|
+
else
|
|
195
|
+
puts " (No environments)"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
puts ""
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def deployment_status(deployment)
|
|
203
|
+
unless quiet?
|
|
204
|
+
say "# Status of last deployment of #{deployment.app_environment.hierarchy_name}:"
|
|
205
|
+
say "#"
|
|
206
|
+
show_deployment(deployment)
|
|
207
|
+
say "#"
|
|
208
|
+
end
|
|
209
|
+
deployment_result(deployment)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def show_deployment(dep)
|
|
213
|
+
return if quiet?
|
|
214
|
+
output = []
|
|
215
|
+
output << ["Account", dep.app.account.name]
|
|
216
|
+
output << ["Application", dep.app.name]
|
|
217
|
+
output << ["Environment", dep.environment.name]
|
|
218
|
+
output << ["Input Ref", dep.ref]
|
|
219
|
+
output << ["Resolved Ref", dep.resolved_ref]
|
|
220
|
+
output << ["Commit", dep.commit || '(not resolved)']
|
|
221
|
+
output << ["Migrate", dep.migrate]
|
|
222
|
+
output << ["Migrate command", dep.migrate_command] if dep.migrate
|
|
223
|
+
output << ["Deployed by", dep.deployed_by]
|
|
224
|
+
output << ["Started at", dep.created_at] if dep.created_at
|
|
225
|
+
output << ["Finished at", dep.finished_at] if dep.finished_at
|
|
226
|
+
|
|
227
|
+
output.each do |att, val|
|
|
228
|
+
puts "#\t%-16s %s" % ["#{att}:", val.to_s]
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def deployment_result(dep)
|
|
233
|
+
if dep.successful?
|
|
234
|
+
say 'Deployment was successful.', :green
|
|
235
|
+
elsif dep.finished_at.nil?
|
|
236
|
+
say 'Deployment is not finished.', :yellow
|
|
237
|
+
else
|
|
238
|
+
say 'Deployment failed.', :red
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def print_exception(e)
|
|
243
|
+
if e.message.empty? || (e.message == e.class.to_s)
|
|
244
|
+
message = nil
|
|
245
|
+
else
|
|
246
|
+
message = e.message
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
if ENV["DEBUG"]
|
|
250
|
+
error(e.class, message)
|
|
251
|
+
e.backtrace.each{|l| say(" "*3 + l) }
|
|
252
|
+
else
|
|
253
|
+
error(message || e.class.to_s)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def print_help(table)
|
|
258
|
+
print_table(table, ident: 2, truncate: true, colwidth: 20)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def set_color(string, color, bold=false)
|
|
262
|
+
($stdout.tty? || ENV['THOR_SHELL']) ? super : string
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def err
|
|
266
|
+
$stderr
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def out
|
|
270
|
+
$stdout
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|