engineyard 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|