engineyard 0.3.2 → 0.3.3
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/lib/engineyard.rb +7 -6
- data/lib/engineyard/api.rb +10 -11
- data/lib/engineyard/cli.rb +21 -82
- data/lib/engineyard/cli/action/deploy.rb +17 -9
- data/lib/engineyard/cli/api.rb +2 -11
- data/lib/engineyard/cli/recipes.rb +14 -0
- data/lib/engineyard/cli/thor_fixes.rb +1 -1
- data/lib/engineyard/cli/ui.rb +18 -9
- data/lib/engineyard/collection.rb +5 -0
- data/lib/engineyard/collection/environments.rb +36 -0
- data/lib/engineyard/error.rb +10 -0
- data/lib/engineyard/model/app.rb +5 -1
- data/lib/engineyard/model/environment.rb +8 -0
- data/lib/engineyard/model/instance.rb +1 -1
- data/lib/engineyard/thor.rb +47 -0
- data/spec/engineyard/collection/environments.rb +75 -0
- data/spec/engineyard/model/environment_spec.rb +37 -0
- data/spec/ey/deploy_spec.rb +8 -6
- data/spec/ey/list_environments_spec.rb +0 -1
- data/spec/ey/logs_spec.rb +14 -0
- data/spec/ey/rebuild_spec.rb +1 -1
- data/spec/ey/ssh_spec.rb +23 -7
- data/spec/ey/upload_recipes_spec.rb +42 -9
- data/spec/spec_helper.rb +0 -4
- data/spec/support/fake_awsm.ru +45 -64
- data/spec/support/helpers.rb +16 -9
- data/spec/support/ruby_ext.rb +0 -16
- metadata +8 -3
data/lib/engineyard.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
module EY
|
2
2
|
require 'engineyard/ruby_ext'
|
3
3
|
|
4
|
-
VERSION = "0.3.
|
4
|
+
VERSION = "0.3.3"
|
5
5
|
|
6
|
-
autoload :API,
|
7
|
-
autoload :
|
8
|
-
autoload :
|
9
|
-
autoload :
|
10
|
-
autoload :
|
6
|
+
autoload :API, 'engineyard/api'
|
7
|
+
autoload :Collection, 'engineyard/collection'
|
8
|
+
autoload :Config, 'engineyard/config'
|
9
|
+
autoload :Error, 'engineyard/error'
|
10
|
+
autoload :Model, 'engineyard/model'
|
11
|
+
autoload :Repo, 'engineyard/repo'
|
11
12
|
|
12
13
|
class UI
|
13
14
|
# stub debug outside of the CLI
|
data/lib/engineyard/api.rb
CHANGED
@@ -30,10 +30,6 @@ module EY
|
|
30
30
|
@apps ||= EY::Model::App.from_array(request('/apps')["apps"], :api => self)
|
31
31
|
end
|
32
32
|
|
33
|
-
def environment_named(name, envs = self.environments)
|
34
|
-
envs.find{|e| e.name == name }
|
35
|
-
end
|
36
|
-
|
37
33
|
def app_for_repo(repo)
|
38
34
|
apps.find{|a| repo.urls.include?(a.repository_uri) }
|
39
35
|
end
|
@@ -72,14 +68,17 @@ module EY
|
|
72
68
|
rescue OpenSSL::SSL::SSLError
|
73
69
|
raise RequestFailed, "SSL is misconfigured on your cloud"
|
74
70
|
end
|
75
|
-
raise RequestFailed, "Response body was empty" if resp.body.empty?
|
76
71
|
|
77
|
-
|
78
|
-
data =
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
72
|
+
if resp.code == 204
|
73
|
+
data = nil
|
74
|
+
else
|
75
|
+
begin
|
76
|
+
data = JSON.parse(resp.body)
|
77
|
+
EY.ui.debug("Response", data)
|
78
|
+
rescue JSON::ParserError
|
79
|
+
EY.ui.debug("Raw response", resp.body)
|
80
|
+
raise RequestFailed, "Response was not valid JSON."
|
81
|
+
end
|
83
82
|
end
|
84
83
|
|
85
84
|
data
|
data/lib/engineyard/cli.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require 'thor'
|
2
1
|
require 'engineyard'
|
3
2
|
require 'engineyard/error'
|
4
|
-
require 'engineyard/
|
3
|
+
require 'engineyard/thor'
|
5
4
|
|
6
5
|
module EY
|
7
|
-
class CLI < Thor
|
8
|
-
autoload :API,
|
9
|
-
autoload :UI,
|
6
|
+
class CLI < EY::Thor
|
7
|
+
autoload :API, 'engineyard/cli/api'
|
8
|
+
autoload :UI, 'engineyard/cli/ui'
|
9
|
+
autoload :Recipes, 'engineyard/cli/recipes'
|
10
10
|
|
11
11
|
include Thor::Actions
|
12
12
|
|
@@ -28,54 +28,36 @@ module EY
|
|
28
28
|
EY::CLI::Action::Deploy.call(env_name, branch, options)
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
31
|
desc "environments [--all]", "List cloud environments for this app, or all environments"
|
33
32
|
method_option :all, :type => :boolean, :aliases => %(-a)
|
34
33
|
def environments
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
EY.ui.print_envs(envs, EY.config.default_environment)
|
39
|
-
elsif envs
|
40
|
-
EY.ui.say %|Cloud environments:|
|
41
|
-
EY.ui.print_envs(envs, EY.config.default_environment)
|
42
|
-
else
|
43
|
-
EY.ui.say %|You do not have any cloud environments.|
|
44
|
-
end
|
34
|
+
apps = get_apps(options[:all])
|
35
|
+
EY.ui.warn(NoAppError.new(repo).message) unless apps.any? || options[:all]
|
36
|
+
EY.ui.print_envs(apps, EY.config.default_environment)
|
45
37
|
end
|
46
38
|
map "envs" => :environments
|
47
39
|
|
48
40
|
desc "rebuild [ENV]", "Rebuild environment (ensure configuration is up-to-date)"
|
49
41
|
def rebuild(name = nil)
|
50
|
-
env =
|
51
|
-
env = api.environment_named(name) or raise NoEnvironmentError.new(name)
|
52
|
-
end
|
53
|
-
|
54
|
-
unless env
|
55
|
-
repo = Repo.new
|
56
|
-
app = api.app_for_repo(repo) or raise NoAppError.new(repo)
|
57
|
-
env = app.one_and_only_environment or raise EnvironmentError, "Unable to determine a single environment for the current application (found #{app.environments.size} environments)"
|
58
|
-
end
|
59
|
-
|
42
|
+
env = fetch_environment(name)
|
60
43
|
EY.ui.debug("Rebuilding #{env.name}")
|
61
44
|
env.rebuild
|
62
45
|
end
|
63
46
|
|
64
|
-
desc "ssh ENV", "Open an ssh session to the environment's application server"
|
65
|
-
def ssh(name)
|
66
|
-
env =
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
raise NoAppMaster.new(env.name)
|
47
|
+
desc "ssh [ENV]", "Open an ssh session to the environment's application server"
|
48
|
+
def ssh(name = nil)
|
49
|
+
env = fetch_environment(name)
|
50
|
+
|
51
|
+
if env.app_master
|
52
|
+
Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}"
|
71
53
|
else
|
72
|
-
|
54
|
+
raise NoAppMaster.new(env.name)
|
73
55
|
end
|
74
56
|
end
|
75
57
|
|
76
|
-
desc "logs ENV", "Retrieve the latest logs for an
|
77
|
-
def logs(name)
|
78
|
-
|
58
|
+
desc "logs [ENV]", "Retrieve the latest logs for an environment"
|
59
|
+
def logs(name = nil)
|
60
|
+
fetch_environment(name).logs.each do |log|
|
79
61
|
EY.ui.info log.instance_name
|
80
62
|
|
81
63
|
if log.main
|
@@ -90,14 +72,8 @@ module EY
|
|
90
72
|
end
|
91
73
|
end
|
92
74
|
|
93
|
-
desc "
|
94
|
-
|
95
|
-
if env_named(name).upload_recipes
|
96
|
-
EY.ui.say "Recipes uploaded successfully"
|
97
|
-
else
|
98
|
-
EY.ui.error "Recipes upload failed"
|
99
|
-
end
|
100
|
-
end
|
75
|
+
desc "recipes COMMAND [ARGS]", "Commands related to custom recipes"
|
76
|
+
subcommand "recipes", EY::CLI::Recipes
|
101
77
|
|
102
78
|
desc "version", "Print the version of the engineyard gem"
|
103
79
|
def version
|
@@ -105,42 +81,5 @@ module EY
|
|
105
81
|
end
|
106
82
|
map ["-v", "--version"] => :version
|
107
83
|
|
108
|
-
private
|
109
|
-
def api
|
110
|
-
@api ||= EY::CLI::API.new
|
111
|
-
end
|
112
|
-
|
113
|
-
def repo
|
114
|
-
@repo ||= EY::Repo.new
|
115
|
-
end
|
116
|
-
|
117
|
-
def env_named(env_name)
|
118
|
-
env = api.environment_named(env_name)
|
119
|
-
|
120
|
-
if env.nil?
|
121
|
-
raise EnvironmentError, "Environment '#{env_name}' can't be found\n" +
|
122
|
-
"You can create it at #{EY.config.endpoint}"
|
123
|
-
end
|
124
|
-
|
125
|
-
env
|
126
|
-
end
|
127
|
-
|
128
|
-
def app_and_envs(all_envs = false)
|
129
|
-
app = api.app_for_repo(repo)
|
130
|
-
|
131
|
-
if all_envs || !app
|
132
|
-
envs = api.environments
|
133
|
-
EY.ui.warn(NoAppError.new(repo).message) unless app || all_envs
|
134
|
-
[nil, envs]
|
135
|
-
else
|
136
|
-
envs = app.environments
|
137
|
-
if envs.empty?
|
138
|
-
EY.ui.warn %|You have no environments set up for the application "#{app.name}"|
|
139
|
-
EY.ui.warn %|You can make one at #{EY.config.endpoint}|
|
140
|
-
end
|
141
|
-
envs.empty? ? [app, nil] : [app, envs]
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
84
|
end # CLI
|
146
85
|
end # EY
|
@@ -8,14 +8,10 @@ module EY
|
|
8
8
|
def self.call(env_name, branch, options)
|
9
9
|
env_name ||= EY.config.default_environment
|
10
10
|
|
11
|
-
app
|
12
|
-
env
|
11
|
+
app = fetch_app
|
12
|
+
env = fetch_environment(env_name, app)
|
13
13
|
branch = fetch_branch(env.name, branch, options[:force])
|
14
|
-
|
15
|
-
running = env.app_master && env.app_master.status == "running"
|
16
|
-
raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}" unless running
|
17
|
-
|
18
|
-
master = env.app_master
|
14
|
+
master = fetch_app_master(env)
|
19
15
|
|
20
16
|
EY.ui.info "Connecting to the server..."
|
21
17
|
ensure_eysd_present(master, options[:install_eysd])
|
@@ -32,6 +28,18 @@ module EY
|
|
32
28
|
|
33
29
|
private
|
34
30
|
|
31
|
+
def self.fetch_app_master(env)
|
32
|
+
master = env.app_master
|
33
|
+
|
34
|
+
if !master
|
35
|
+
raise EnvironmentError, "No running instances for environment #{env.name}\nStart one at #{EY.config.endpoint}"
|
36
|
+
elsif master.status != "running"
|
37
|
+
raise EnvironmentError, "Cannot deploy: application master's status is not \"running\" (green); it is \"#{master.status}\"."
|
38
|
+
end
|
39
|
+
|
40
|
+
master
|
41
|
+
end
|
42
|
+
|
35
43
|
def self.api
|
36
44
|
@api ||= EY::CLI::API.new
|
37
45
|
end
|
@@ -52,13 +60,13 @@ module EY
|
|
52
60
|
raise DeployArgumentError if !env_name && app.environments.size != 1
|
53
61
|
|
54
62
|
env = if env_name
|
55
|
-
|
63
|
+
app.environments.match_one(env_name)
|
56
64
|
else
|
57
65
|
app.environments.first
|
58
66
|
end
|
59
67
|
|
60
68
|
# the environment exists, but doesn't have this app
|
61
|
-
if !env && api.
|
69
|
+
if !env && api.environments.named(env_name)
|
62
70
|
raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
|
63
71
|
end
|
64
72
|
|
data/lib/engineyard/cli/api.rb
CHANGED
@@ -23,8 +23,8 @@ module EY
|
|
23
23
|
@token = self.class.fetch_token
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
|
26
|
+
def fetch_app_for_repo(repo)
|
27
|
+
app_for_repo(repo) || raise(NoAppError.new(repo))
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.fetch_token
|
@@ -41,15 +41,6 @@ module EY
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
private
|
45
|
-
def find_environment_by_unambiguous_substring(env_name, envs)
|
46
|
-
candidates = envs.find_all{|e| e.name[env_name] }
|
47
|
-
if candidates.size > 1
|
48
|
-
raise AmbiguousEnvironmentName.new(env_name, candidates.map {|e| e.name})
|
49
|
-
end
|
50
|
-
candidates.first
|
51
|
-
end
|
52
|
-
|
53
44
|
end
|
54
45
|
end
|
55
46
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module EY
|
2
|
+
class CLI
|
3
|
+
class Recipes < EY::Thor
|
4
|
+
desc "recipes upload [ENV]", "Upload custom chef recipes from the current directory to ENV"
|
5
|
+
def upload(name = nil)
|
6
|
+
if fetch_environment(name).upload_recipes
|
7
|
+
EY.ui.say "Recipes uploaded successfully"
|
8
|
+
else
|
9
|
+
EY.ui.error "Recipes upload failed"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/engineyard/cli/ui.rb
CHANGED
@@ -59,17 +59,26 @@ module EY
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
-
def print_envs(
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
def print_envs(apps, default_env_name = nil)
|
63
|
+
apps.each do |app|
|
64
|
+
puts app.name
|
65
|
+
if app.environments.any?
|
66
|
+
app.environments.each do |env|
|
67
|
+
short_name = env.shorten_name_for(app)
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
icount = env.instances_count
|
70
|
+
iname = (icount == 1) ? "instance" : "instances"
|
71
|
+
|
72
|
+
default_text = env.name == default_env_name ? " [default]" : ""
|
73
|
+
|
74
|
+
puts " #{short_name}#{default_text} (#{icount} #{iname})"
|
75
|
+
end
|
76
|
+
else
|
77
|
+
puts " (This application is not in any environments; you can make one at #{EY.config.endpoint})"
|
78
|
+
end
|
79
|
+
|
80
|
+
puts ""
|
71
81
|
end
|
72
|
-
print_table(printable_envs, :ident => 2)
|
73
82
|
end
|
74
83
|
|
75
84
|
def print_exception(e)
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'engineyard/error'
|
2
|
+
|
3
|
+
module EY
|
4
|
+
module Collection
|
5
|
+
class Environments < Array
|
6
|
+
|
7
|
+
def named(name)
|
8
|
+
find {|e| e.name == name}
|
9
|
+
end
|
10
|
+
|
11
|
+
def named!(name)
|
12
|
+
named(name) or raise EnvironmentError,
|
13
|
+
"Environment '#{name}' can't be found\nYou can create it at #{EY.config.endpoint}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def match_one(name_part)
|
17
|
+
named(name_part) || find_by_unambiguous_substring(name_part)
|
18
|
+
end
|
19
|
+
|
20
|
+
def match_one!(name_part)
|
21
|
+
match_one(name_part) or raise EnvironmentError,
|
22
|
+
"Environment containing '#{name_part}' can't be found\nYou can create it at #{EY.config.endpoint}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def find_by_unambiguous_substring(name_part)
|
27
|
+
candidates = find_all{|e| e.name[name_part] }
|
28
|
+
if candidates.size > 1
|
29
|
+
raise AmbiguousEnvironmentName.new(name_part, candidates.map {|e| e.name})
|
30
|
+
end
|
31
|
+
candidates.first
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/engineyard/error.rb
CHANGED
@@ -44,6 +44,16 @@ module EY
|
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
+
class NoSingleEnvironmentError < EY::Error
|
48
|
+
def initialize(app)
|
49
|
+
@envs = app.environments
|
50
|
+
end
|
51
|
+
|
52
|
+
def message
|
53
|
+
"Unable to determine a single environment for the current application (found #{@envs.size} environments)"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
47
57
|
class NoEnvironmentError < EY::Error
|
48
58
|
def initialize(env_name=nil)
|
49
59
|
@env_name = env_name
|
data/lib/engineyard/model/app.rb
CHANGED
@@ -8,12 +8,16 @@ module EY
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def sole_environment
|
12
12
|
if environments.size == 1
|
13
13
|
environments.first
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
+
def sole_environment!
|
18
|
+
sole_environment or raise NoSingleEnvironmentError.new(self)
|
19
|
+
end
|
20
|
+
|
17
21
|
end
|
18
22
|
end
|
19
23
|
end
|
@@ -9,6 +9,10 @@ module EY
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
+
def self.from_array(array, extras={})
|
13
|
+
Collection::Environments[*super]
|
14
|
+
end
|
15
|
+
|
12
16
|
def logs
|
13
17
|
Log.from_array(api_get("/environments/#{id}/logs")["logs"])
|
14
18
|
end
|
@@ -48,6 +52,10 @@ module EY
|
|
48
52
|
EY.config.environments[self.name]
|
49
53
|
end
|
50
54
|
alias_method :config, :configuration
|
55
|
+
|
56
|
+
def shorten_name_for(app)
|
57
|
+
name.gsub(/^#{Regexp.quote(app.name)}_/, '')
|
58
|
+
end
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|
@@ -3,7 +3,7 @@ require 'escape'
|
|
3
3
|
module EY
|
4
4
|
module Model
|
5
5
|
class Instance < ApiStruct.new(:id, :role, :status, :amazon_id, :public_hostname, :environment)
|
6
|
-
EYSD_VERSION = "~>0.3.
|
6
|
+
EYSD_VERSION = "~>0.3.3"
|
7
7
|
CHECK_SCRIPT = <<-SCRIPT
|
8
8
|
require "rubygems"
|
9
9
|
requirement = Gem::Requirement.new("#{EYSD_VERSION}")
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'engineyard/cli/thor_fixes'
|
3
|
+
|
4
|
+
module EY
|
5
|
+
class Thor < ::Thor
|
6
|
+
def self.start(original_args=ARGV, config={})
|
7
|
+
@@original_args = original_args
|
8
|
+
super
|
9
|
+
end
|
10
|
+
|
11
|
+
no_tasks do
|
12
|
+
def subcommand_args
|
13
|
+
@@original_args[1..-1]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.subcommand(subcommand, subcommand_class)
|
17
|
+
define_method(subcommand) { |*_| subcommand_class.start(subcommand_args) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def api
|
24
|
+
@api ||= EY::CLI::API.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def repo
|
28
|
+
@repo ||= EY::Repo.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_environment(env_name)
|
32
|
+
if env_name.nil?
|
33
|
+
api.fetch_app_for_repo(repo).sole_environment!
|
34
|
+
else
|
35
|
+
api.environments.match_one!(env_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def get_apps(all_apps = false)
|
40
|
+
if all_apps
|
41
|
+
api.apps
|
42
|
+
else
|
43
|
+
[api.app_for_repo(repo)].compact
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EY::Collection::Environments do
|
4
|
+
before(:each) do
|
5
|
+
@envs = described_class.new([
|
6
|
+
EY::Model::Environment.from_hash("id" => 1234, "name" => "app_production"),
|
7
|
+
EY::Model::Environment.from_hash("id" => 4321, "name" => "app_staging"),
|
8
|
+
EY::Model::Environment.from_hash("id" => 8765, "name" => "bigapp_staging"),
|
9
|
+
])
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#match_one" do
|
13
|
+
it "works when given an unambiguous substring" do
|
14
|
+
@envs.match_one("prod").name.should == "app_production"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises an error when given an ambiguous substring" do
|
18
|
+
lambda {
|
19
|
+
@envs.match_one("staging")
|
20
|
+
}.should raise_error(EY::AmbiguousEnvironmentName)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns an exact match if one exists" do
|
24
|
+
@envs.match_one("app_staging").name.should == "app_staging"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns nil when it can't find anything" do
|
28
|
+
@envs.match_one("dev-and-production").should be_nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#match_one!" do
|
33
|
+
it "works when given an unambiguous substring" do
|
34
|
+
@envs.match_one!("prod").name.should == "app_production"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "raises an error when given an ambiguous substring" do
|
38
|
+
lambda {
|
39
|
+
@envs.match_one!("staging")
|
40
|
+
}.should raise_error(EY::AmbiguousEnvironmentName)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "returns an exact match if one exists" do
|
44
|
+
@envs.match_one!("app_staging").name.should == "app_staging"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises an error when it can't find anything" do
|
48
|
+
lambda {
|
49
|
+
@envs.match_one!("dev-and-production")
|
50
|
+
}.should raise_error(EY::EnvironmentError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#named" do
|
55
|
+
it "finds the environment with the matching name" do
|
56
|
+
@envs.named("app_staging").id.should == 4321
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns nil when no name matches" do
|
60
|
+
@envs.named("something else").should be_nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#named!" do
|
65
|
+
it "finds the environment with the matching name" do
|
66
|
+
@envs.named!("app_staging").id.should == 4321
|
67
|
+
end
|
68
|
+
|
69
|
+
it "raises an error when no name matches" do
|
70
|
+
lambda {
|
71
|
+
@envs.named!("something else")
|
72
|
+
}.should raise_error(EY::EnvironmentError)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -19,6 +19,19 @@ describe "EY::Model::Environment#rebuild" do
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
describe "EY::Model::Environment.from_array" do
|
23
|
+
it "returns a smart collection, not just a dumb array" do
|
24
|
+
api_data = [
|
25
|
+
{"id" => 32340, "name" => 'iceberg'},
|
26
|
+
{"id" => 9433, "name" => 'zoidberg'},
|
27
|
+
]
|
28
|
+
|
29
|
+
collection = EY::Model::Environment.from_array(api_data)
|
30
|
+
collection.should be_kind_of(Array)
|
31
|
+
collection.should respond_to(:match_one)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
22
35
|
describe "EY::Model::Environment#instances" do
|
23
36
|
it_should_behave_like "it has an api"
|
24
37
|
|
@@ -43,3 +56,27 @@ describe "EY::Model::Environment#instances" do
|
|
43
56
|
env.instances.first.should == EY::Model::Instance.from_hash(instance_data.merge(:environment => env))
|
44
57
|
end
|
45
58
|
end
|
59
|
+
|
60
|
+
describe "EY::Model::Environment#shorten_name_for(app)" do
|
61
|
+
def short(environment_name, app_name)
|
62
|
+
env = EY::Model::Environment.from_hash({:name => environment_name})
|
63
|
+
app = EY::Model::App.from_hash({:name => app_name})
|
64
|
+
env.shorten_name_for(app)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "turns myapp+myapp_production to production" do
|
68
|
+
short('myapp_production', 'myapp').should == 'production'
|
69
|
+
end
|
70
|
+
|
71
|
+
it "turns product+production to product (leaves it alone)" do
|
72
|
+
short('production', 'product').should == 'production'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "leaves the environment name alone when the app name appears in the middle" do
|
76
|
+
short('hattery', 'ate').should == 'hattery'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "does not produce an empty string when the names are the same" do
|
80
|
+
short('dev', 'dev').should == 'dev'
|
81
|
+
end
|
82
|
+
end
|
data/spec/ey/deploy_spec.rb
CHANGED
@@ -49,6 +49,13 @@ describe "ey deploy" do
|
|
49
49
|
ey "deploy", :expect_failure => true
|
50
50
|
@err.should match(/was called incorrectly/i)
|
51
51
|
end
|
52
|
+
|
53
|
+
it "complains when the app master is in a non-running state" do
|
54
|
+
api_scenario "one app, one environment, app master red"
|
55
|
+
ey "deploy giblets master", :expect_failure => true
|
56
|
+
@err.should_not match(/No running instances/i)
|
57
|
+
@err.should match(/running.*\(green\)/)
|
58
|
+
end
|
52
59
|
end
|
53
60
|
|
54
61
|
it "runs when environment is known" do
|
@@ -88,12 +95,7 @@ describe "ey deploy" do
|
|
88
95
|
|
89
96
|
context "choosing something to deploy" do
|
90
97
|
before(:all) do
|
91
|
-
api_scenario "one app, one environment"
|
92
|
-
api_git_remote("user@git.host/path/to/repo.git")
|
93
|
-
end
|
94
|
-
|
95
|
-
after(:all) do
|
96
|
-
api_git_remote(nil)
|
98
|
+
api_scenario "one app, one environment", "user@git.host/path/to/repo.git"
|
97
99
|
end
|
98
100
|
|
99
101
|
before(:all) do
|
data/spec/ey/logs_spec.rb
CHANGED
@@ -10,6 +10,20 @@ describe "ey logs" do
|
|
10
10
|
@out.should match(/CUSTOM LOG OUTPUT/)
|
11
11
|
@err.should be_empty
|
12
12
|
end
|
13
|
+
|
14
|
+
it "can infer the environment" do
|
15
|
+
api_scenario "one app, one environment"
|
16
|
+
ey "logs"
|
17
|
+
@out.should match(/MAIN LOG OUTPUT/)
|
18
|
+
@out.should match(/CUSTOM LOG OUTPUT/)
|
19
|
+
@err.should be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
it "complains when it can't infer the environment" do
|
23
|
+
api_scenario "one app, two environments"
|
24
|
+
ey "logs", :expect_failure => true
|
25
|
+
@err.should =~ /single environment/
|
26
|
+
end
|
13
27
|
end
|
14
28
|
|
15
29
|
describe "ey logs ENV" do
|
data/spec/ey/rebuild_spec.rb
CHANGED
data/spec/ey/ssh_spec.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
print_my_args_ssh = "#!/bin/sh\necho ssh $*"
|
4
|
+
|
3
5
|
describe "ey ssh" do
|
4
6
|
it_should_behave_like "an integration test"
|
5
7
|
|
@@ -8,9 +10,7 @@ describe "ey ssh" do
|
|
8
10
|
end
|
9
11
|
|
10
12
|
it "SSH-es into the right environment" do
|
11
|
-
|
12
|
-
|
13
|
-
ey "ssh giblets", :prepend_to_path => {'ssh' => print_my_args}
|
13
|
+
ey "ssh giblets", :prepend_to_path => {'ssh' => print_my_args_ssh}
|
14
14
|
@raw_ssh_commands.should == ["ssh turkey@174.129.198.124"]
|
15
15
|
end
|
16
16
|
|
@@ -20,11 +20,27 @@ describe "ey ssh" do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "complains if you give it a bogus environment" do
|
23
|
-
|
24
|
-
|
25
|
-
ey "ssh bogusenv", :prepend_to_path => {'ssh' => print_my_args}, :hide_err => true
|
23
|
+
ey "ssh bogusenv", :prepend_to_path => {'ssh' => print_my_args_ssh}, :expect_failure => true
|
26
24
|
@raw_ssh_commands.should be_empty
|
27
|
-
@
|
25
|
+
@err.should =~ /bogusenv/
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "ey ssh" do
|
30
|
+
it_should_behave_like "an integration test"
|
31
|
+
|
32
|
+
it "guesses the environment from the current application" do
|
33
|
+
api_scenario "one app, one environment"
|
34
|
+
|
35
|
+
ey "ssh", :prepend_to_path => {'ssh' => print_my_args_ssh}
|
36
|
+
@raw_ssh_commands.should == ["ssh turkey@174.129.198.124"]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "complains when it can't guess the environment and its name isn't specified" do
|
40
|
+
api_scenario "one app, one environment, not linked"
|
41
|
+
|
42
|
+
ey "ssh", :prepend_to_path => {'ssh' => print_my_args_ssh}, :expect_failure => true
|
43
|
+
@err.should =~ /single environment/i
|
28
44
|
end
|
29
45
|
end
|
30
46
|
|
@@ -1,21 +1,54 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe "ey
|
3
|
+
describe "ey recipes upload" do
|
4
4
|
it_should_behave_like "an integration test"
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
File.open(dir.join("cookbooks/file"), "w"){|f| f << "boo" }
|
6
|
+
before(:all) do
|
7
|
+
@recipe_dir = Pathname.new("/tmp/#{$$}")
|
8
|
+
@recipe_dir.mkdir
|
9
|
+
Dir.chdir(@recipe_dir) do
|
10
|
+
@recipe_dir.join("cookbooks").mkdir
|
11
|
+
File.open(@recipe_dir.join("cookbooks/file"), "w"){|f| f << "boo" }
|
13
12
|
`git init`
|
14
13
|
`git add .`
|
15
14
|
`git commit -m "OMG"`
|
16
|
-
|
15
|
+
`git remote add testremote user@host.tld:path/to/repo.git`
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "posts the recipes to the correct url" do
|
20
|
+
api_scenario "one app, one environment"
|
21
|
+
Dir.chdir(@recipe_dir) do
|
22
|
+
ey "recipes upload giblets", :debug => true
|
23
|
+
end
|
24
|
+
|
25
|
+
@out.should =~ /recipes uploaded successfully/i
|
26
|
+
end
|
27
|
+
|
28
|
+
it "errors correctly on bogus env name" do
|
29
|
+
api_scenario "one app, one environment"
|
30
|
+
ey "recipes upload bogusenv", :expect_failure => true
|
31
|
+
|
32
|
+
@err.should =~ /can't be found/i
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can infer the environment from the current application" do
|
36
|
+
api_scenario "one app, one environment", "user@host.tld:path/to/repo.git"
|
37
|
+
|
38
|
+
Dir.chdir(@recipe_dir) do
|
39
|
+
ey "recipes upload", :debug => true
|
17
40
|
end
|
18
41
|
|
19
42
|
@out.should =~ /recipes uploaded successfully/i
|
20
43
|
end
|
44
|
+
|
45
|
+
it "complains when it can't infer the environment from the current application" do
|
46
|
+
api_scenario "one app, one environment, not linked", "user@host.tld:path/to/repo.git"
|
47
|
+
|
48
|
+
Dir.chdir(@recipe_dir) do
|
49
|
+
ey "recipes upload", :debug => true, :expect_failure => true
|
50
|
+
end
|
51
|
+
|
52
|
+
@err.should =~ /single environment/i
|
53
|
+
end
|
21
54
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/support/fake_awsm.ru
CHANGED
@@ -28,26 +28,18 @@ class FakeAwsm < Sinatra::Base
|
|
28
28
|
Scenario::UnlinkedApp
|
29
29
|
when "one app, one environment"
|
30
30
|
Scenario::LinkedApp
|
31
|
+
when "one app, one environment, app master red"
|
32
|
+
Scenario::LinkedAppRedMaster
|
31
33
|
when "one app, two environments"
|
32
34
|
Scenario::OneAppTwoEnvs
|
33
35
|
when "one app, many similarly-named environments"
|
34
36
|
Scenario::OneAppManySimilarlyNamedEnvs
|
37
|
+
else
|
38
|
+
status(400)
|
39
|
+
return {"ok" => "false", "message" => "wtf is the #{params[:scenario]} scenario?"}.to_json
|
35
40
|
end
|
36
|
-
|
37
|
-
|
38
|
-
{"ok" => "true"}.to_json
|
39
|
-
else
|
40
|
-
status(400)
|
41
|
-
{"ok" => "false", "message" => "wtf is the #{params[:scenario]} scenario?"}.to_json
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
put "/git_remote" do
|
46
|
-
FindableGitRemote.remote = if (!params[:remote] || params[:remote].empty?)
|
47
|
-
nil
|
48
|
-
else
|
49
|
-
params[:remote]
|
50
|
-
end
|
41
|
+
@@scenario = new_scenario.new(params[:remote])
|
42
|
+
{"ok" => "true"}.to_json
|
51
43
|
end
|
52
44
|
|
53
45
|
get "/api/v2/apps" do
|
@@ -67,7 +59,8 @@ class FakeAwsm < Sinatra::Base
|
|
67
59
|
end
|
68
60
|
|
69
61
|
put "/api/v2/environments/:env_id/rebuild" do
|
70
|
-
|
62
|
+
status(204)
|
63
|
+
""
|
71
64
|
end
|
72
65
|
|
73
66
|
post "/api/v2/authenticate" do
|
@@ -86,54 +79,32 @@ private
|
|
86
79
|
params[:password] == "test"
|
87
80
|
end
|
88
81
|
|
89
|
-
module
|
90
|
-
class
|
91
|
-
|
92
|
-
end
|
93
|
-
|
94
|
-
def git_remote
|
95
|
-
FindableGitRemote.remote || local_git_remote
|
96
|
-
end
|
82
|
+
module Scenario
|
83
|
+
class Empty
|
84
|
+
attr_reader :git_remote
|
97
85
|
|
98
|
-
|
99
|
-
|
100
|
-
# simulate this by faking out the API to have whatever git
|
101
|
-
# remote we'll find anyway.
|
102
|
-
def local_git_remote
|
103
|
-
remotes = []
|
104
|
-
`git remote -v`.each_line do |line|
|
105
|
-
parts = line.split(/\t/)
|
106
|
-
# the remote will look like
|
107
|
-
# "git@github.com:engineyard/engineyard.git (fetch)\n"
|
108
|
-
# so we need to chop it up a bit
|
109
|
-
remotes << parts[1].gsub(/\s.*$/, "") if parts[1]
|
86
|
+
def initialize(git_remote)
|
87
|
+
@git_remote = git_remote
|
110
88
|
end
|
111
|
-
remotes.first
|
112
|
-
end
|
113
|
-
end
|
114
89
|
|
115
|
-
|
116
|
-
class Empty
|
117
|
-
def self.apps
|
90
|
+
def apps
|
118
91
|
[]
|
119
92
|
end
|
120
93
|
|
121
|
-
def
|
94
|
+
def environments
|
122
95
|
[]
|
123
96
|
end
|
124
97
|
end # Empty
|
125
98
|
|
126
|
-
class UnlinkedApp
|
127
|
-
|
128
|
-
|
129
|
-
def self.apps
|
99
|
+
class UnlinkedApp < Empty
|
100
|
+
def apps
|
130
101
|
[{
|
131
102
|
"name" => "rails232app",
|
132
103
|
"environments" => [],
|
133
104
|
"repository_uri" => git_remote}]
|
134
105
|
end
|
135
106
|
|
136
|
-
def
|
107
|
+
def environments
|
137
108
|
[{
|
138
109
|
"ssh_username" => "turkey",
|
139
110
|
"instances" => [{
|
@@ -152,10 +123,8 @@ private
|
|
152
123
|
end
|
153
124
|
end # UnlinkedApp
|
154
125
|
|
155
|
-
class LinkedApp
|
156
|
-
|
157
|
-
|
158
|
-
def self.apps
|
126
|
+
class LinkedApp < Empty
|
127
|
+
def apps
|
159
128
|
[{"name" => "rails232app",
|
160
129
|
"environments" => [{"ssh_username" => "turkey",
|
161
130
|
"instances" => [{"public_hostname" => "174.129.198.124",
|
@@ -173,7 +142,7 @@ private
|
|
173
142
|
"repository_uri" => git_remote}]
|
174
143
|
end
|
175
144
|
|
176
|
-
def
|
145
|
+
def environments
|
177
146
|
[{
|
178
147
|
"ssh_username" => "turkey",
|
179
148
|
"instances" => [{
|
@@ -193,7 +162,7 @@ private
|
|
193
162
|
"id" => 27220}}]
|
194
163
|
end
|
195
164
|
|
196
|
-
def
|
165
|
+
def logs(env_id)
|
197
166
|
[{
|
198
167
|
"id" => env_id,
|
199
168
|
"role" => "app_master",
|
@@ -203,10 +172,24 @@ private
|
|
203
172
|
end
|
204
173
|
end # LinkedApp
|
205
174
|
|
206
|
-
class
|
207
|
-
|
175
|
+
class LinkedAppRedMaster < LinkedApp
|
176
|
+
def apps
|
177
|
+
apps = super
|
178
|
+
apps[0]["environments"][0]["instances"][0]["status"] = "error"
|
179
|
+
apps[0]["environments"][0]["app_master"]["status"] = "error"
|
180
|
+
apps
|
181
|
+
end
|
208
182
|
|
209
|
-
def
|
183
|
+
def environments
|
184
|
+
envs = super
|
185
|
+
envs[0]["instances"][0]["status"] = "error"
|
186
|
+
envs[0]["app_master"]["status"] = "error"
|
187
|
+
envs
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class OneAppTwoEnvs < Empty
|
192
|
+
def apps
|
210
193
|
apps = [{
|
211
194
|
"name" => "rails232app",
|
212
195
|
"repository_uri" => git_remote
|
@@ -239,7 +222,7 @@ private
|
|
239
222
|
"repository_uri" => git_remote}]
|
240
223
|
end
|
241
224
|
|
242
|
-
def
|
225
|
+
def environments
|
243
226
|
[{
|
244
227
|
"ssh_username" => "turkey",
|
245
228
|
"instances" => [{
|
@@ -272,10 +255,8 @@ private
|
|
272
255
|
end
|
273
256
|
end # OneAppTwoEnvs
|
274
257
|
|
275
|
-
class OneAppManySimilarlyNamedEnvs
|
276
|
-
|
277
|
-
|
278
|
-
def self.apps
|
258
|
+
class OneAppManySimilarlyNamedEnvs < Empty
|
259
|
+
def apps
|
279
260
|
apps = [{
|
280
261
|
"name" => "rails232app",
|
281
262
|
"repository_uri" => git_remote
|
@@ -336,7 +317,7 @@ private
|
|
336
317
|
"repository_uri" => git_remote}]
|
337
318
|
end
|
338
319
|
|
339
|
-
def
|
320
|
+
def environments
|
340
321
|
[{
|
341
322
|
"ssh_username" => "turkey",
|
342
323
|
"instances" => [{
|
@@ -395,7 +376,7 @@ private
|
|
395
376
|
}]
|
396
377
|
end
|
397
378
|
|
398
|
-
def
|
379
|
+
def logs(env_id)
|
399
380
|
[{
|
400
381
|
"id" => env_id,
|
401
382
|
"role" => "app_master",
|
data/spec/support/helpers.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require '
|
1
|
+
require 'realweb'
|
2
2
|
require "rest_client"
|
3
3
|
require 'open4'
|
4
4
|
|
@@ -42,8 +42,8 @@ module Spec
|
|
42
42
|
with_env(ey_env) do
|
43
43
|
exit_status = Open4::open4("#{eybin} #{cmd}") do |pid, stdin, stdout, stderr|
|
44
44
|
block.call(stdin) if block
|
45
|
-
@err = stderr.
|
46
|
-
@out = stdout.
|
45
|
+
@err = stderr.read
|
46
|
+
@out = stdout.read
|
47
47
|
end
|
48
48
|
|
49
49
|
if !exit_status.success? && !options[:expect_failure]
|
@@ -75,14 +75,21 @@ module Spec
|
|
75
75
|
@out
|
76
76
|
end
|
77
77
|
|
78
|
-
def api_scenario(scenario)
|
79
|
-
response = ::RestClient.put(EY.fake_awsm + '/scenario', {"scenario" => scenario}, {})
|
78
|
+
def api_scenario(scenario, remote = local_git_remote)
|
79
|
+
response = ::RestClient.put(EY.fake_awsm + '/scenario', {"scenario" => scenario, "remote" => remote}, {})
|
80
80
|
raise "Setting scenario failed: #{response.inspect}" unless response.code == 200
|
81
81
|
end
|
82
82
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
83
|
+
def local_git_remote
|
84
|
+
remotes = []
|
85
|
+
`git remote -v`.each_line do |line|
|
86
|
+
parts = line.split(/\t/)
|
87
|
+
# the remote will look like
|
88
|
+
# "git@github.com:engineyard/engineyard.git (fetch)\n"
|
89
|
+
# so we need to chop it up a bit
|
90
|
+
remotes << parts[1].gsub(/\s.*$/, "") if parts[1]
|
91
|
+
end
|
92
|
+
remotes.first
|
86
93
|
end
|
87
94
|
|
88
95
|
def read_yaml(file="ey.yml")
|
@@ -125,7 +132,7 @@ module EY
|
|
125
132
|
raise SyntaxError, "There is a syntax error in fake_awsm.ru! fix it!"
|
126
133
|
end
|
127
134
|
config_ru = File.join(EY_ROOT, "spec/support/fake_awsm.ru")
|
128
|
-
@server =
|
135
|
+
@server = RealWeb.start_server_in_fork(config_ru)
|
129
136
|
"http://localhost:#{@server.port}"
|
130
137
|
end
|
131
138
|
end
|
data/spec/support/ruby_ext.rb
CHANGED
@@ -11,19 +11,3 @@ module Kernel
|
|
11
11
|
end
|
12
12
|
alias capture_stdout capture_stdio
|
13
13
|
end
|
14
|
-
|
15
|
-
class IO
|
16
|
-
def read_available_bytes(chunk_size = 1024, select_timeout = 5)
|
17
|
-
buffer = []
|
18
|
-
|
19
|
-
while self.class.select([self], nil, nil, select_timeout)
|
20
|
-
begin
|
21
|
-
buffer << self.readpartial(chunk_size)
|
22
|
-
rescue(EOFError)
|
23
|
-
break
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
return buffer.join
|
28
|
-
end
|
29
|
-
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 3
|
8
|
-
-
|
9
|
-
version: 0.3.
|
8
|
+
- 3
|
9
|
+
version: 0.3.3
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- EY Cloud Team
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-06-03 00:00:00 -07:00
|
18
18
|
default_executable: ey
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -111,9 +111,12 @@ files:
|
|
111
111
|
- lib/engineyard/api.rb
|
112
112
|
- lib/engineyard/cli/action/deploy.rb
|
113
113
|
- lib/engineyard/cli/api.rb
|
114
|
+
- lib/engineyard/cli/recipes.rb
|
114
115
|
- lib/engineyard/cli/thor_fixes.rb
|
115
116
|
- lib/engineyard/cli/ui.rb
|
116
117
|
- lib/engineyard/cli.rb
|
118
|
+
- lib/engineyard/collection/environments.rb
|
119
|
+
- lib/engineyard/collection.rb
|
117
120
|
- lib/engineyard/config.rb
|
118
121
|
- lib/engineyard/error.rb
|
119
122
|
- lib/engineyard/model/api_struct.rb
|
@@ -124,6 +127,7 @@ files:
|
|
124
127
|
- lib/engineyard/model.rb
|
125
128
|
- lib/engineyard/repo.rb
|
126
129
|
- lib/engineyard/ruby_ext.rb
|
130
|
+
- lib/engineyard/thor.rb
|
127
131
|
- lib/engineyard.rb
|
128
132
|
- LICENSE
|
129
133
|
- README.rdoc
|
@@ -161,6 +165,7 @@ test_files:
|
|
161
165
|
- spec/engineyard/api_spec.rb
|
162
166
|
- spec/engineyard/cli/api_spec.rb
|
163
167
|
- spec/engineyard/cli_spec.rb
|
168
|
+
- spec/engineyard/collection/environments.rb
|
164
169
|
- spec/engineyard/config_spec.rb
|
165
170
|
- spec/engineyard/model/api_struct_spec.rb
|
166
171
|
- spec/engineyard/model/environment_spec.rb
|