engineyard 0.2.9 → 0.2.10
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/engineyard.rb +1 -17
- data/lib/engineyard/account.rb +13 -49
- data/lib/engineyard/account/app.rb +18 -0
- data/lib/engineyard/account/app_master.rb +12 -0
- data/lib/engineyard/account/environment.rb +31 -0
- data/lib/engineyard/account/log.rb +18 -0
- data/lib/engineyard/api.rb +4 -2
- data/lib/engineyard/cli.rb +34 -11
- data/lib/engineyard/cli/ui.rb +1 -1
- data/spec/engineyard/api_spec.rb +6 -6
- data/spec/engineyard/cli/api_spec.rb +1 -1
- data/spec/engineyard/config_spec.rb +9 -9
- data/spec/ey/deploy_spec.rb +40 -21
- data/spec/ey/logs_spec.rb +28 -0
- data/spec/spec_helper.rb +12 -13
- data/spec/support/fake_awsm.ru +251 -0
- data/spec/support/helpers.rb +50 -38
- data/spec/support/ruby_ext.rb +29 -0
- metadata +52 -102
- data/lib/vendor/thor.rb +0 -244
- data/lib/vendor/thor/actions.rb +0 -275
- data/lib/vendor/thor/actions/create_file.rb +0 -103
- data/lib/vendor/thor/actions/directory.rb +0 -91
- data/lib/vendor/thor/actions/empty_directory.rb +0 -134
- data/lib/vendor/thor/actions/file_manipulation.rb +0 -223
- data/lib/vendor/thor/actions/inject_into_file.rb +0 -104
- data/lib/vendor/thor/base.rb +0 -540
- data/lib/vendor/thor/core_ext/file_binary_read.rb +0 -9
- data/lib/vendor/thor/core_ext/hash_with_indifferent_access.rb +0 -75
- data/lib/vendor/thor/core_ext/ordered_hash.rb +0 -100
- data/lib/vendor/thor/error.rb +0 -30
- data/lib/vendor/thor/group.rb +0 -271
- data/lib/vendor/thor/invocation.rb +0 -180
- data/lib/vendor/thor/parser.rb +0 -4
- data/lib/vendor/thor/parser/argument.rb +0 -67
- data/lib/vendor/thor/parser/arguments.rb +0 -150
- data/lib/vendor/thor/parser/option.rb +0 -128
- data/lib/vendor/thor/parser/options.rb +0 -169
- data/lib/vendor/thor/rake_compat.rb +0 -66
- data/lib/vendor/thor/runner.rb +0 -314
- data/lib/vendor/thor/shell.rb +0 -83
- data/lib/vendor/thor/shell/basic.rb +0 -239
- data/lib/vendor/thor/shell/color.rb +0 -108
- data/lib/vendor/thor/task.rb +0 -102
- data/lib/vendor/thor/util.rb +0 -230
- data/lib/vendor/thor/version.rb +0 -3
data/lib/engineyard.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module EY
|
2
|
-
VERSION = "0.2.
|
2
|
+
VERSION = "0.2.10"
|
3
3
|
|
4
4
|
autoload :Account, 'engineyard/account'
|
5
5
|
autoload :API, 'engineyard/api'
|
@@ -24,21 +24,5 @@ module EY
|
|
24
24
|
@config ||= EY::Config.new
|
25
25
|
end
|
26
26
|
|
27
|
-
def config=(config)
|
28
|
-
@config = config
|
29
|
-
end
|
30
|
-
|
31
|
-
def library(libname)
|
32
|
-
begin
|
33
|
-
require libname
|
34
|
-
rescue LoadError
|
35
|
-
unless @tried_rubygems
|
36
|
-
require 'rubygems' rescue LoadError nil
|
37
|
-
@tried_rubygems = true
|
38
|
-
retry
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
27
|
end
|
44
28
|
end
|
data/lib/engineyard/account.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
require 'engineyard/account/app'
|
2
|
+
require 'engineyard/account/app_master'
|
3
|
+
require 'engineyard/account/environment'
|
4
|
+
require 'engineyard/account/log'
|
5
|
+
|
1
6
|
module EY
|
2
7
|
class Account
|
3
8
|
|
@@ -5,10 +10,14 @@ module EY
|
|
5
10
|
@api = api
|
6
11
|
end
|
7
12
|
|
13
|
+
def request(path, options = { })
|
14
|
+
@api.request(path, {:method => :get}.merge(options))
|
15
|
+
end
|
16
|
+
|
8
17
|
def environments
|
9
18
|
return @environments if @environments
|
10
|
-
data =
|
11
|
-
@environments = Environment.from_array(data || [])
|
19
|
+
data = request('/environments')["environments"]
|
20
|
+
@environments = Environment.from_array(data || [], self)
|
12
21
|
end
|
13
22
|
|
14
23
|
def environment_named(name)
|
@@ -17,8 +26,8 @@ module EY
|
|
17
26
|
|
18
27
|
def apps
|
19
28
|
return @apps if @apps
|
20
|
-
data = @api.request('/apps'
|
21
|
-
@apps = App.from_array(data || [])
|
29
|
+
data = @api.request('/apps')["apps"]
|
30
|
+
@apps = App.from_array(data || [], self)
|
22
31
|
end
|
23
32
|
|
24
33
|
def app_named(name)
|
@@ -29,50 +38,5 @@ module EY
|
|
29
38
|
apps.find{|a| repo.urls.include?(a.repository_url) }
|
30
39
|
end
|
31
40
|
|
32
|
-
# Classes to represent the returned data
|
33
|
-
class Environment < Struct.new(:name, :instances_count, :apps, :app_master, :username)
|
34
|
-
def self.from_hash(hash)
|
35
|
-
new(
|
36
|
-
hash["name"],
|
37
|
-
hash["instances_count"],
|
38
|
-
App.from_array(hash["apps"]),
|
39
|
-
AppMaster.from_hash(hash["app_master"]),
|
40
|
-
hash["ssh_username"]
|
41
|
-
) if hash && hash != "null"
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.from_array(array)
|
45
|
-
array.map{|n| from_hash(n) } if array && array != "null"
|
46
|
-
end
|
47
|
-
|
48
|
-
def configuration
|
49
|
-
EY.config.environments[self.name]
|
50
|
-
end
|
51
|
-
alias_method :config, :configuration
|
52
|
-
end
|
53
|
-
|
54
|
-
class App < Struct.new(:name, :repository_url, :environments)
|
55
|
-
def self.from_hash(hash)
|
56
|
-
new(
|
57
|
-
hash["name"],
|
58
|
-
hash["repository_uri"], # We use url canonically in the ey gem
|
59
|
-
Environment.from_array(hash["environments"])
|
60
|
-
) if hash && hash != "null"
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.from_array(array)
|
64
|
-
array.map{|n| from_hash(n) } if array && array != "null"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
class AppMaster < Struct.new(:status, :public_hostname)
|
69
|
-
def self.from_hash(hash)
|
70
|
-
new(
|
71
|
-
hash["status"],
|
72
|
-
hash["public_hostname"]
|
73
|
-
) if hash && hash != "null"
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
41
|
end # Account
|
78
42
|
end # EY
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EY
|
2
|
+
class Account
|
3
|
+
class App < Struct.new(:name, :repository_url, :environments, :account)
|
4
|
+
def self.from_hash(hash, account)
|
5
|
+
new(
|
6
|
+
hash["name"],
|
7
|
+
hash["repository_uri"], # We use url canonically in the ey gem
|
8
|
+
Environment.from_array(hash["environments"], account),
|
9
|
+
account
|
10
|
+
) if hash && hash != "null"
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_array(array, account)
|
14
|
+
array.map{|n| from_hash(n, account) } if array && array != "null"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EY
|
2
|
+
class Account
|
3
|
+
class Environment < Struct.new(:id, :name, :instances_count, :apps, :app_master, :username, :account)
|
4
|
+
def self.from_hash(hash, account)
|
5
|
+
new(
|
6
|
+
hash["id"],
|
7
|
+
hash["name"],
|
8
|
+
hash["instances_count"],
|
9
|
+
App.from_array(hash["apps"], account),
|
10
|
+
AppMaster.from_hash(hash["app_master"]),
|
11
|
+
hash["ssh_username"],
|
12
|
+
account
|
13
|
+
) if hash && hash != "null"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_array(array, account)
|
17
|
+
array.map{|n| from_hash(n, account) } if array && array != "null"
|
18
|
+
end
|
19
|
+
|
20
|
+
def logs
|
21
|
+
data = account.request("/environments/#{id}/logs")['logs']
|
22
|
+
Log.from_array(data || [])
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration
|
26
|
+
EY.config.environments[self.name]
|
27
|
+
end
|
28
|
+
alias_method :config, :configuration
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Log < Struct.new(:id, :role, :main, :custom)
|
2
|
+
def self.from_hash(hash)
|
3
|
+
new(
|
4
|
+
hash["id"],
|
5
|
+
hash["role"],
|
6
|
+
hash["main"],
|
7
|
+
hash["custom"]
|
8
|
+
) if hash && hash != "null"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.from_array(array)
|
12
|
+
array.map{|n| from_hash(n) } if array && array != "null"
|
13
|
+
end
|
14
|
+
|
15
|
+
def instance_name
|
16
|
+
"#{role} #{id}"
|
17
|
+
end
|
18
|
+
end
|
data/lib/engineyard/api.rb
CHANGED
@@ -24,8 +24,8 @@ module EY
|
|
24
24
|
class RequestFailed < EY::Error; end
|
25
25
|
|
26
26
|
def self.request(path, opts={})
|
27
|
-
|
28
|
-
|
27
|
+
require 'rest_client'
|
28
|
+
require 'json'
|
29
29
|
|
30
30
|
url = EY.config.endpoint + "api/v2#{path}"
|
31
31
|
method = ((meth = opts.delete(:method)) && meth.to_s || "get").downcase.to_sym
|
@@ -50,6 +50,8 @@ module EY
|
|
50
50
|
raise RequestFailed, "The requested resource could not be found"
|
51
51
|
rescue RestClient::RequestFailed => e
|
52
52
|
raise RequestFailed, "#{e.message}"
|
53
|
+
rescue OpenSSL::SSL::SSLError
|
54
|
+
raise RequestFailed, "SSL is misconfigured on your cloud"
|
53
55
|
end
|
54
56
|
raise RequestFailed, "Response body was empty" if resp.body.empty?
|
55
57
|
|
data/lib/engineyard/cli.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
$:.unshift File.expand_path('../../vendor', __FILE__)
|
2
1
|
require 'thor'
|
3
|
-
|
4
2
|
require 'engineyard'
|
5
3
|
require 'engineyard/cli/error'
|
6
4
|
|
7
5
|
module EY
|
8
6
|
class CLI < Thor
|
9
|
-
EYSD_VERSION = "~>0.2.
|
7
|
+
EYSD_VERSION = "~>0.2.5"
|
10
8
|
|
11
9
|
autoload :API, 'engineyard/cli/api'
|
12
10
|
autoload :UI, 'engineyard/cli/ui'
|
@@ -22,6 +20,7 @@ module EY
|
|
22
20
|
method_option :force, :type => :boolean, :aliases => %w(-f),
|
23
21
|
:desc => "Force a deploy of the specified branch"
|
24
22
|
method_option :migrate, :type => :string, :aliases => %w(-m),
|
23
|
+
:default => 'rake db:migrate',
|
25
24
|
:desc => "Run migrations via [MIGRATE], defaults to 'rake db:migrate'"
|
26
25
|
method_option :install_eysd, :type => :boolean, :aliases => %(-s),
|
27
26
|
:desc => "Force remote install of eysd"
|
@@ -39,7 +38,7 @@ module EY
|
|
39
38
|
invalid_branch = default_branch && (branch != default_branch) && !options[:force]
|
40
39
|
raise BranchMismatch.new(default_branch, branch) if invalid_branch
|
41
40
|
|
42
|
-
if env_name
|
41
|
+
if env_name && app.environments
|
43
42
|
env = app.environments.find{|e| e.name == env_name }
|
44
43
|
else
|
45
44
|
env = app.environments.first
|
@@ -84,12 +83,8 @@ module EY
|
|
84
83
|
deploy_cmd << " --config '#{escaped_config_option}'"
|
85
84
|
end
|
86
85
|
|
87
|
-
if options
|
88
|
-
|
89
|
-
deploy_cmd << " --migrate='#{options[:migrate]}'"
|
90
|
-
else
|
91
|
-
deploy_cmd << " --no-migrate"
|
92
|
-
end
|
86
|
+
if options['migrate']
|
87
|
+
deploy_cmd << " --migrate='#{options[:migrate]}'"
|
93
88
|
end
|
94
89
|
|
95
90
|
EY.ui.info "Running deploy on server..."
|
@@ -129,6 +124,30 @@ module EY
|
|
129
124
|
end
|
130
125
|
end
|
131
126
|
|
127
|
+
desc "logs environment", "Retrieve the latest logs for an enviornment"
|
128
|
+
def logs(environment)
|
129
|
+
env = account.environment_named(environment)
|
130
|
+
|
131
|
+
if env.nil?
|
132
|
+
raise EnvironmentError, "Environment '#{env_name}' can't be found\n" +
|
133
|
+
"You can create it at #{EY.config.endpoint}"
|
134
|
+
else
|
135
|
+
env.logs.each do |log|
|
136
|
+
EY.ui.info log.instance_name
|
137
|
+
|
138
|
+
if log.main
|
139
|
+
EY.ui.info "Main logs:"
|
140
|
+
EY.ui.say log.main
|
141
|
+
end
|
142
|
+
|
143
|
+
if log.custom
|
144
|
+
EY.ui.info "Custom logs:"
|
145
|
+
EY.ui.say log.custom
|
146
|
+
end
|
147
|
+
end # logs_for_environment(env).each
|
148
|
+
end # env.nil?
|
149
|
+
end
|
150
|
+
|
132
151
|
desc "version", "Print the version of the engineyard gem"
|
133
152
|
def version
|
134
153
|
EY.ui.say %{engineyard version #{EY::VERSION}}
|
@@ -167,7 +186,11 @@ module EY
|
|
167
186
|
cmd << %{ &> /dev/null} unless output
|
168
187
|
EY.ui.debug(cmd)
|
169
188
|
puts cmd if output
|
170
|
-
|
189
|
+
unless ENV["NO_SSH"]
|
190
|
+
system cmd
|
191
|
+
else
|
192
|
+
true
|
193
|
+
end
|
171
194
|
end
|
172
195
|
|
173
196
|
end # CLI
|
data/lib/engineyard/cli/ui.rb
CHANGED
data/spec/engineyard/api_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe EY::API do
|
4
4
|
it "gets the api token from ~/.eyrc if possible" do
|
5
|
-
|
5
|
+
write_yaml({"api_token" => "asdf"}, '~/.eyrc')
|
6
6
|
EY::API.new.should == EY::API.new("asdf")
|
7
7
|
end
|
8
8
|
|
@@ -17,7 +17,7 @@ describe EY::API do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
it "puts the api token into .eyrc" do
|
20
|
-
|
20
|
+
read_yaml('~/.eyrc')["api_token"].should == "asdf"
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
@@ -25,18 +25,18 @@ describe EY::API do
|
|
25
25
|
context "without a custom endpoint" do
|
26
26
|
it "saves the api token at the root of the data" do
|
27
27
|
EY::API.save_token("asdf")
|
28
|
-
|
28
|
+
read_yaml('~/.eyrc')["api_token"].should == "asdf"
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
context "with a custom endpoint" do
|
33
|
-
before do
|
34
|
-
|
33
|
+
before(:each) do
|
34
|
+
write_yaml({"endpoint" => "http://localhost/"}, 'ey.yml')
|
35
35
|
EY::API.save_token("asdf")
|
36
36
|
end
|
37
37
|
|
38
38
|
it "saves the api token" do
|
39
|
-
|
39
|
+
read_yaml('~/.eyrc').should == {"http://localhost/" => {"api_token" => "asdf"}}
|
40
40
|
end
|
41
41
|
|
42
42
|
it "reads the api token" do
|
@@ -22,7 +22,7 @@ describe EY::CLI::API do
|
|
22
22
|
before(:each) do
|
23
23
|
FakeWeb.register_uri(:post, "https://cloud.engineyard.com/api/v2/authenticate", :body => %|{"api_token": "asdf"}|)
|
24
24
|
|
25
|
-
|
25
|
+
capture_stdio("\n\n") do
|
26
26
|
@token = EY::CLI::API.new
|
27
27
|
end
|
28
28
|
end
|
@@ -4,12 +4,12 @@ require 'uri'
|
|
4
4
|
describe EY::Config do
|
5
5
|
describe "environments" do
|
6
6
|
it "get loaded from the config file" do
|
7
|
-
|
7
|
+
write_yaml("environments" => {"production" => {"default" => true}})
|
8
8
|
EY::Config.new.environments["production"]["default"].should be_true
|
9
9
|
end
|
10
10
|
|
11
11
|
it "are present when the config file has no environments key" do
|
12
|
-
|
12
|
+
write_yaml("endpoint" => "http://localhost/")
|
13
13
|
EY::Config.new.environments.should == {}
|
14
14
|
end
|
15
15
|
end
|
@@ -20,36 +20,36 @@ describe EY::Config do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it "gets loaded from the config file" do
|
23
|
-
|
23
|
+
write_yaml("endpoint" => "http://localhost/")
|
24
24
|
EY::Config.new.endpoint.should == URI.parse("http://localhost/")
|
25
25
|
end
|
26
26
|
|
27
27
|
it "raises on an invalid endpoint" do
|
28
|
-
|
28
|
+
write_yaml("endpoint" => "non/absolute")
|
29
29
|
lambda { EY::Config.new.endpoint }.
|
30
30
|
should raise_error(EY::Config::ConfigurationError)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
34
|
it "provides default_endpoint?" do
|
35
|
-
|
35
|
+
write_yaml("endpoint" => "http://localhost/")
|
36
36
|
EY::Config.new.default_endpoint?.should_not be_true
|
37
37
|
end
|
38
38
|
|
39
39
|
describe "files" do
|
40
40
|
it "looks for config/ey.yml" do
|
41
|
-
|
42
|
-
|
41
|
+
write_yaml({"endpoint" => "http://something/"}, "ey.yml")
|
42
|
+
write_yaml({"endpoint" => "http://localhost/"}, "config/ey.yml")
|
43
43
|
EY::Config.new.endpoint.should == URI.parse("http://localhost/")
|
44
44
|
end
|
45
45
|
|
46
46
|
it "looks for ey.yml" do
|
47
|
-
|
47
|
+
write_yaml({"endpoint" => "http://foo/"}, "ey.yml")
|
48
48
|
EY::Config.new.endpoint.should == URI.parse("http://foo/")
|
49
49
|
end
|
50
50
|
|
51
51
|
it "looks for the file given" do
|
52
|
-
|
52
|
+
write_yaml({"endpoint" => "http://bar/"}, "summat.yml")
|
53
53
|
EY::Config.new("summat.yml").endpoint.should == URI.parse("http://bar/")
|
54
54
|
end
|
55
55
|
end
|
data/spec/ey/deploy_spec.rb
CHANGED
@@ -2,14 +2,16 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe "ey deploy" do
|
4
4
|
before(:all) do
|
5
|
-
ENV['EYRC'] = "/tmp/eyrc"
|
6
|
-
ENV['CLOUD_URL'] = "http://localhost:4000"
|
7
5
|
FakeFS.deactivate!
|
6
|
+
ENV['EYRC'] = "/tmp/eyrc"
|
7
|
+
ENV['CLOUD_URL'] = EY.fake_awsm
|
8
|
+
FakeWeb.allow_net_connect = true
|
8
9
|
end
|
9
10
|
|
10
11
|
after(:all) do
|
11
12
|
ENV['CLOUD_URL'] = nil
|
12
13
|
FakeFS.activate!
|
14
|
+
FakeWeb.allow_net_connect = false
|
13
15
|
end
|
14
16
|
|
15
17
|
describe "without an eyrc file" do
|
@@ -18,10 +20,11 @@ describe "ey deploy" do
|
|
18
20
|
end
|
19
21
|
|
20
22
|
it "prompts for authentication" do
|
21
|
-
ey("deploy") do |input|
|
22
|
-
input.puts("
|
23
|
-
input.puts("
|
23
|
+
ey("deploy", :hide_err => true) do |input|
|
24
|
+
input.puts("test@test.test")
|
25
|
+
input.puts("test")
|
24
26
|
end
|
27
|
+
|
25
28
|
@out.should include("We need to fetch your API token, please login")
|
26
29
|
@out.should include("Email:")
|
27
30
|
@out.should include("Password:")
|
@@ -36,30 +39,46 @@ describe "ey deploy" do
|
|
36
39
|
end
|
37
40
|
|
38
41
|
it "complains when there is no app" do
|
39
|
-
|
42
|
+
api_scenario "empty"
|
40
43
|
ey "deploy", :hide_err => true
|
41
|
-
@err.should include
|
44
|
+
@err.should include(%|no application configured|)
|
42
45
|
end
|
43
46
|
|
44
|
-
it "complains when there is no environment" do
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
@out.should match(/no environment/i)
|
47
|
+
it "complains when there is no environment for the app" do
|
48
|
+
api_scenario "one app, one environment, not linked"
|
49
|
+
ey "deploy giblets master", :hide_err => true
|
50
|
+
@err.should match(/doesn't run this application/i)
|
49
51
|
end
|
50
52
|
|
51
53
|
it "runs when environment is known" do
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
@
|
54
|
+
api_scenario "one app, one environment"
|
55
|
+
ey "deploy", :hide_err => true
|
56
|
+
@out.should match(/running deploy/i)
|
57
|
+
@err.should be_empty
|
56
58
|
end
|
57
59
|
|
58
60
|
it "complains when environment is ambiguous" do
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
61
|
+
api_scenario "one app, two environments"
|
62
|
+
ey "deploy", :hide_err => true
|
63
|
+
@err.should match(/was called incorrectly/i)
|
64
|
+
end
|
65
|
+
|
66
|
+
context "migration command" do
|
67
|
+
before(:each) do
|
68
|
+
api_scenario "one app, one environment"
|
69
|
+
end
|
70
|
+
|
71
|
+
it "defaults to 'rake db:migrate'" do
|
72
|
+
ey "deploy"
|
73
|
+
@ssh_commands.size.should == 1
|
74
|
+
@ssh_commands.first.should =~ /--migrate='rake db:migrate'/
|
75
|
+
end
|
76
|
+
|
77
|
+
it "can be disabled with --no-migrate" do
|
78
|
+
ey "deploy --no-migrate"
|
79
|
+
@ssh_commands.size.should == 1
|
80
|
+
@ssh_commands.first.should_not =~ /--migrate/
|
81
|
+
end
|
63
82
|
end
|
64
83
|
end
|
65
|
-
end
|
84
|
+
end
|