engineyard 0.3.1 → 0.3.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.
- data/lib/engineyard.rb +2 -2
- data/lib/engineyard/api.rb +19 -0
- data/lib/engineyard/cli.rb +24 -12
- data/lib/engineyard/cli/action/deploy.rb +112 -0
- data/lib/engineyard/cli/api.rb +14 -1
- data/lib/engineyard/error.rb +27 -0
- data/lib/engineyard/model.rb +9 -0
- data/lib/engineyard/{account → model}/api_struct.rb +8 -3
- data/lib/engineyard/{account → model}/app.rb +3 -3
- data/lib/engineyard/{account → model}/environment.rb +14 -7
- data/lib/engineyard/model/instance.rb +91 -0
- data/lib/engineyard/{account → model}/log.rb +1 -1
- data/lib/engineyard/repo.rb +3 -3
- data/spec/engineyard/{account → model}/api_struct_spec.rb +11 -7
- data/spec/engineyard/model/environment_spec.rb +45 -0
- data/spec/engineyard/model/instance_spec.rb +70 -0
- data/spec/engineyard/repo_spec.rb +17 -14
- data/spec/ey/deploy_spec.rb +95 -45
- data/spec/ey/list_environments_spec.rb +11 -1
- data/spec/ey/logs_spec.rb +18 -0
- data/spec/ey/rebuild_spec.rb +21 -4
- data/spec/ey/ssh_spec.rb +28 -3
- data/spec/spec_helper.rb +37 -12
- data/spec/support/fake_awsm.ru +139 -4
- data/spec/support/helpers.rb +19 -8
- metadata +31 -20
- data/lib/engineyard/account.rb +0 -63
- data/lib/engineyard/account/app_master.rb +0 -6
- data/lib/engineyard/account/instance.rb +0 -6
- data/lib/engineyard/action/deploy.rb +0 -138
- data/lib/engineyard/action/rebuild.rb +0 -31
- data/lib/engineyard/action/util.rb +0 -19
- data/spec/engineyard/account/environment_spec.rb +0 -20
- data/spec/engineyard/account_spec.rb +0 -18
data/lib/engineyard.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module EY
|
2
2
|
require 'engineyard/ruby_ext'
|
3
3
|
|
4
|
-
VERSION = "0.3.
|
4
|
+
VERSION = "0.3.2"
|
5
5
|
|
6
|
-
autoload :Account, 'engineyard/account'
|
7
6
|
autoload :API, 'engineyard/api'
|
8
7
|
autoload :Config, 'engineyard/config'
|
9
8
|
autoload :Error, 'engineyard/error'
|
9
|
+
autoload :Model, 'engineyard/model'
|
10
10
|
autoload :Repo, 'engineyard/repo'
|
11
11
|
|
12
12
|
class UI
|
data/lib/engineyard/api.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'engineyard/model'
|
2
|
+
|
1
3
|
module EY
|
2
4
|
class API
|
3
5
|
attr_reader :token
|
@@ -20,9 +22,26 @@ module EY
|
|
20
22
|
self.class.request(url, opts)
|
21
23
|
end
|
22
24
|
|
25
|
+
def environments
|
26
|
+
@environments ||= EY::Model::Environment.from_array(request('/environments')["environments"], :api => self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def apps
|
30
|
+
@apps ||= EY::Model::App.from_array(request('/apps')["apps"], :api => self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def environment_named(name, envs = self.environments)
|
34
|
+
envs.find{|e| e.name == name }
|
35
|
+
end
|
36
|
+
|
37
|
+
def app_for_repo(repo)
|
38
|
+
apps.find{|a| repo.urls.include?(a.repository_uri) }
|
39
|
+
end
|
40
|
+
|
23
41
|
class InvalidCredentials < EY::Error; end
|
24
42
|
class RequestFailed < EY::Error; end
|
25
43
|
|
44
|
+
|
26
45
|
def self.request(path, opts={})
|
27
46
|
require 'rest_client'
|
28
47
|
require 'json'
|
data/lib/engineyard/cli.rb
CHANGED
@@ -24,8 +24,8 @@ module EY
|
|
24
24
|
method_option :install_eysd, :type => :boolean, :aliases => %(-s),
|
25
25
|
:desc => "Force remote install of eysd"
|
26
26
|
def deploy(env_name = nil, branch = nil)
|
27
|
-
require 'engineyard/action/deploy'
|
28
|
-
EY::Action::Deploy.call(env_name, branch, options)
|
27
|
+
require 'engineyard/cli/action/deploy'
|
28
|
+
EY::CLI::Action::Deploy.call(env_name, branch, options)
|
29
29
|
end
|
30
30
|
|
31
31
|
|
@@ -47,15 +47,27 @@ module EY
|
|
47
47
|
|
48
48
|
desc "rebuild [ENV]", "Rebuild environment (ensure configuration is up-to-date)"
|
49
49
|
def rebuild(name = nil)
|
50
|
-
|
51
|
-
|
50
|
+
env = if name
|
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
|
+
|
60
|
+
EY.ui.debug("Rebuilding #{env.name}")
|
61
|
+
env.rebuild
|
52
62
|
end
|
53
63
|
|
54
64
|
desc "ssh ENV", "Open an ssh session to the environment's application server"
|
55
65
|
def ssh(name)
|
56
|
-
env =
|
57
|
-
if env
|
66
|
+
env = api.environment_named(name)
|
67
|
+
if env && env.app_master
|
58
68
|
Kernel.exec "ssh", "#{env.username}@#{env.app_master.public_hostname}", *ARGV[2..-1]
|
69
|
+
elsif env
|
70
|
+
raise NoAppMaster.new(env.name)
|
59
71
|
else
|
60
72
|
EY.ui.warn %|Could not find an environment named "#{name}"|
|
61
73
|
end
|
@@ -80,7 +92,7 @@ module EY
|
|
80
92
|
|
81
93
|
desc "upload_recipes ENV", "Upload custom chef recipes from the current directory to ENV"
|
82
94
|
def upload_recipes(name)
|
83
|
-
if
|
95
|
+
if env_named(name).upload_recipes
|
84
96
|
EY.ui.say "Recipes uploaded successfully"
|
85
97
|
else
|
86
98
|
EY.ui.error "Recipes upload failed"
|
@@ -94,8 +106,8 @@ module EY
|
|
94
106
|
map ["-v", "--version"] => :version
|
95
107
|
|
96
108
|
private
|
97
|
-
def
|
98
|
-
@
|
109
|
+
def api
|
110
|
+
@api ||= EY::CLI::API.new
|
99
111
|
end
|
100
112
|
|
101
113
|
def repo
|
@@ -103,7 +115,7 @@ module EY
|
|
103
115
|
end
|
104
116
|
|
105
117
|
def env_named(env_name)
|
106
|
-
env =
|
118
|
+
env = api.environment_named(env_name)
|
107
119
|
|
108
120
|
if env.nil?
|
109
121
|
raise EnvironmentError, "Environment '#{env_name}' can't be found\n" +
|
@@ -114,10 +126,10 @@ module EY
|
|
114
126
|
end
|
115
127
|
|
116
128
|
def app_and_envs(all_envs = false)
|
117
|
-
app =
|
129
|
+
app = api.app_for_repo(repo)
|
118
130
|
|
119
131
|
if all_envs || !app
|
120
|
-
envs =
|
132
|
+
envs = api.environments
|
121
133
|
EY.ui.warn(NoAppError.new(repo).message) unless app || all_envs
|
122
134
|
[nil, envs]
|
123
135
|
else
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module EY
|
2
|
+
class CLI
|
3
|
+
module Action
|
4
|
+
class Deploy
|
5
|
+
|
6
|
+
EYSD_VERSION = "~>0.3.0"
|
7
|
+
|
8
|
+
def self.call(env_name, branch, options)
|
9
|
+
env_name ||= EY.config.default_environment
|
10
|
+
|
11
|
+
app = fetch_app
|
12
|
+
env = fetch_environment(env_name, app)
|
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
|
19
|
+
|
20
|
+
EY.ui.info "Connecting to the server..."
|
21
|
+
ensure_eysd_present(master, options[:install_eysd])
|
22
|
+
|
23
|
+
EY.ui.info "Running deploy for '#{env.name}' on server..."
|
24
|
+
deployed = master.deploy!(app, branch, options[:migrate], env.config)
|
25
|
+
|
26
|
+
if deployed
|
27
|
+
EY.ui.info "Deploy complete"
|
28
|
+
else
|
29
|
+
raise EY::Error, "Deploy failed"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def self.api
|
36
|
+
@api ||= EY::CLI::API.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.repo
|
40
|
+
@repo ||= EY::Repo.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.fetch_app
|
44
|
+
app = api.app_for_repo(repo)
|
45
|
+
raise NoAppError.new(repo) unless app
|
46
|
+
app
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.fetch_environment(env_name, app)
|
50
|
+
# if the name's not specified and there's not exactly one
|
51
|
+
# environment, we can't figure out which environment to deploy
|
52
|
+
raise DeployArgumentError if !env_name && app.environments.size != 1
|
53
|
+
|
54
|
+
env = if env_name
|
55
|
+
api.environment_named(env_name, app.environments)
|
56
|
+
else
|
57
|
+
app.environments.first
|
58
|
+
end
|
59
|
+
|
60
|
+
# the environment exists, but doesn't have this app
|
61
|
+
if !env && api.environment_named(env_name)
|
62
|
+
raise EnvironmentError, "Environment '#{env_name}' doesn't run this application\nYou can add it at #{EY.config.endpoint}"
|
63
|
+
end
|
64
|
+
|
65
|
+
if !env
|
66
|
+
raise NoEnvironmentError.new(env_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
env
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.fetch_branch(env_name, user_specified_branch, force)
|
73
|
+
default_branch = EY.config.default_branch(env_name)
|
74
|
+
|
75
|
+
branch = if user_specified_branch
|
76
|
+
if default_branch && (user_specified_branch != default_branch) && !force
|
77
|
+
raise BranchMismatch.new(default_branch, user_specified_branch)
|
78
|
+
end
|
79
|
+
user_specified_branch
|
80
|
+
else
|
81
|
+
default_branch || repo.current_branch
|
82
|
+
end
|
83
|
+
|
84
|
+
raise DeployArgumentError unless branch
|
85
|
+
branch
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.ensure_eysd_present(instance, install_eysd)
|
89
|
+
eysd_status = instance.ey_deploy_check
|
90
|
+
case eysd_status
|
91
|
+
when :ssh_failed
|
92
|
+
raise EnvironmentError, "SSH connection to #{instance.hostname} failed"
|
93
|
+
when :eysd_missing
|
94
|
+
EY.ui.warn "Instance does not have server-side component installed"
|
95
|
+
EY.ui.info "Installing server-side component..."
|
96
|
+
instance.install_ey_deploy!
|
97
|
+
when :too_new
|
98
|
+
raise EnvironmentError, "server-side component too new; please upgrade your copy of the engineyard gem."
|
99
|
+
when :too_old
|
100
|
+
EY.ui.info "Upgrading server-side component..."
|
101
|
+
instance.upgrade_ey_deploy!
|
102
|
+
when :ok
|
103
|
+
# no action needed
|
104
|
+
else
|
105
|
+
raise EY::Error, "Internal error: Unexpected status from Instance#ey_deploy_check; got #{eysd_status.inspect}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
data/lib/engineyard/cli/api.rb
CHANGED
@@ -23,6 +23,10 @@ module EY
|
|
23
23
|
@token = self.class.fetch_token
|
24
24
|
end
|
25
25
|
|
26
|
+
def environment_named(env_name, envs = self.environments)
|
27
|
+
super || find_environment_by_unambiguous_substring(env_name, envs)
|
28
|
+
end
|
29
|
+
|
26
30
|
def self.fetch_token
|
27
31
|
EY.ui.warn("The engineyard gem is prerelease software. Please do not use")
|
28
32
|
EY.ui.warn("this tool to deploy to mission-critical environments, yet.")
|
@@ -37,6 +41,15 @@ module EY
|
|
37
41
|
end
|
38
42
|
end
|
39
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
|
+
|
40
53
|
end
|
41
54
|
end
|
42
|
-
end
|
55
|
+
end
|
data/lib/engineyard/error.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
module EY
|
2
2
|
class Error < RuntimeError; end
|
3
3
|
|
4
|
+
class NoRemotesError < EY::Error
|
5
|
+
def initialize(path)
|
6
|
+
super "fatal: No git remotes found in #{path}"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
4
10
|
class NoAppError < Error
|
5
11
|
def initialize(repo)
|
6
12
|
@repo = repo
|
@@ -14,9 +20,30 @@ module EY
|
|
14
20
|
end
|
15
21
|
end
|
16
22
|
|
23
|
+
class NoAppMaster < EY::Error
|
24
|
+
def initialize(env_name)
|
25
|
+
@env_name = env_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
"The environment '#{@env_name}' does not have a master instance."
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
17
33
|
class EnvironmentError < EY::Error
|
18
34
|
end
|
19
35
|
|
36
|
+
class AmbiguousEnvironmentName < EY::Error
|
37
|
+
def initialize(name, matches)
|
38
|
+
@name, @matches = name, matches
|
39
|
+
end
|
40
|
+
|
41
|
+
def message
|
42
|
+
pretty_names = @matches.map {|x| "'#{x}'"}.join(', ')
|
43
|
+
"The name '#{@name}' is ambiguous; it matches all of the following environment names: #{pretty_names}.\nPlease use a longer, unambiguous substring or the entire environment name."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
20
47
|
class NoEnvironmentError < EY::Error
|
21
48
|
def initialize(env_name=nil)
|
22
49
|
@env_name = env_name
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module EY
|
2
|
+
module Model
|
3
|
+
autoload :ApiStruct, 'engineyard/model/api_struct'
|
4
|
+
autoload :App, 'engineyard/model/app'
|
5
|
+
autoload :Environment, 'engineyard/model/environment'
|
6
|
+
autoload :Log, 'engineyard/model/log'
|
7
|
+
autoload :Instance, 'engineyard/model/instance'
|
8
|
+
end
|
9
|
+
end
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module EY
|
2
|
-
|
2
|
+
module Model
|
3
3
|
class ApiStruct < Struct
|
4
4
|
def self.new(*args, &block)
|
5
5
|
super(*args) do |*block_args|
|
6
6
|
block.call(*block_args) if block
|
7
7
|
|
8
|
-
def self.from_array(array,
|
8
|
+
def self.from_array(array, common_values = {})
|
9
9
|
array.map do |values|
|
10
|
-
from_hash(values.merge(
|
10
|
+
from_hash(values.merge(common_values))
|
11
11
|
end if array
|
12
12
|
end
|
13
13
|
|
@@ -20,6 +20,11 @@ module EY
|
|
20
20
|
|
21
21
|
end
|
22
22
|
end
|
23
|
+
|
24
|
+
def api_get(uri, options = {})
|
25
|
+
api.request(uri, options.merge(:method => :get))
|
26
|
+
end
|
27
|
+
|
23
28
|
end
|
24
29
|
end
|
25
30
|
end
|
@@ -1,10 +1,10 @@
|
|
1
1
|
module EY
|
2
|
-
|
3
|
-
class App < ApiStruct.new(:name, :repository_uri, :environments, :
|
2
|
+
module Model
|
3
|
+
class App < ApiStruct.new(:name, :repository_uri, :environments, :api)
|
4
4
|
|
5
5
|
def self.from_hash(hash)
|
6
6
|
super.tap do |app|
|
7
|
-
app.environments = Environment.from_array(app.environments, app.
|
7
|
+
app.environments = Environment.from_array(app.environments, :api => app.api)
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
@@ -1,24 +1,31 @@
|
|
1
1
|
module EY
|
2
|
-
|
3
|
-
class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :
|
2
|
+
module Model
|
3
|
+
class Environment < ApiStruct.new(:id, :name, :instances_count, :apps, :app_master, :username, :api)
|
4
4
|
def self.from_hash(hash)
|
5
5
|
super.tap do |env|
|
6
6
|
env.username = hash['ssh_username']
|
7
|
-
env.apps = App.from_array(env.apps, env.
|
8
|
-
env.app_master =
|
7
|
+
env.apps = App.from_array(env.apps, :api => env.api)
|
8
|
+
env.app_master = Instance.from_hash(env.app_master.merge(:environment => env)) if env.app_master
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
def logs
|
13
|
-
|
13
|
+
Log.from_array(api_get("/environments/#{id}/logs")["logs"])
|
14
14
|
end
|
15
15
|
|
16
16
|
def instances
|
17
|
-
|
17
|
+
Instance.from_array(api_get("/environments/#{id}/instances")["instances"], :environment => self)
|
18
18
|
end
|
19
19
|
|
20
20
|
def rebuild
|
21
|
-
|
21
|
+
api.request("/environments/#{id}/rebuild", :method => :put)
|
22
|
+
end
|
23
|
+
|
24
|
+
def upload_recipes(file_to_upload = recipe_file)
|
25
|
+
api.request("/environments/#{id}/recipes",
|
26
|
+
:method => :post,
|
27
|
+
:params => {:file => file_to_upload}
|
28
|
+
)
|
22
29
|
end
|
23
30
|
|
24
31
|
def recipe_file
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'escape'
|
2
|
+
|
3
|
+
module EY
|
4
|
+
module Model
|
5
|
+
class Instance < ApiStruct.new(:id, :role, :status, :amazon_id, :public_hostname, :environment)
|
6
|
+
EYSD_VERSION = "~>0.3.2"
|
7
|
+
CHECK_SCRIPT = <<-SCRIPT
|
8
|
+
require "rubygems"
|
9
|
+
requirement = Gem::Requirement.new("#{EYSD_VERSION}")
|
10
|
+
required_version = requirement.requirements.last.last # thanks thanks rubygems rubygems
|
11
|
+
|
12
|
+
# this will be a ["name-version", Gem::Specification] two-element array if present, nil otherwise
|
13
|
+
ey_deploy_geminfo = Gem.source_index.find{ |(name,_)| name =~ /^ey-deploy-\\\d/ }
|
14
|
+
exit(104) unless ey_deploy_geminfo
|
15
|
+
|
16
|
+
current_version = ey_deploy_geminfo.last.version
|
17
|
+
exit(0) if requirement.satisfied_by?(current_version)
|
18
|
+
exit(70) if required_version > current_version
|
19
|
+
exit(17) # required_version < current_version
|
20
|
+
SCRIPT
|
21
|
+
EXIT_STATUS = Hash.new { |h,k| raise EY::Error, "ey-deploy version checker exited with unknown status code #{k}" }
|
22
|
+
EXIT_STATUS.merge!({
|
23
|
+
255 => :ssh_failed,
|
24
|
+
104 => :eysd_missing,
|
25
|
+
70 => :too_old,
|
26
|
+
17 => :too_new,
|
27
|
+
0 => :ok,
|
28
|
+
})
|
29
|
+
|
30
|
+
alias :hostname :public_hostname
|
31
|
+
|
32
|
+
def deploy!(app, ref, migration_command=nil, extra_configuration=nil)
|
33
|
+
deploy_cmd = [eysd_path, 'deploy', '--app', app.name, '--branch', ref]
|
34
|
+
|
35
|
+
if extra_configuration
|
36
|
+
deploy_cmd << '--config' << extra_configuration.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
if migration_command
|
40
|
+
deploy_cmd << "--migrate" << migration_command
|
41
|
+
end
|
42
|
+
|
43
|
+
ssh Escape.shell_command(deploy_cmd)
|
44
|
+
end
|
45
|
+
|
46
|
+
def ey_deploy_check
|
47
|
+
require 'base64'
|
48
|
+
encoded_script = Base64.encode64(CHECK_SCRIPT).gsub(/\n/, '')
|
49
|
+
ssh "#{ruby_path} -r base64 -e \"eval Base64.decode64(ARGV[0])\" #{encoded_script}", false
|
50
|
+
EXIT_STATUS[$?.exitstatus]
|
51
|
+
end
|
52
|
+
|
53
|
+
def install_ey_deploy!
|
54
|
+
ssh(Escape.shell_command(['sudo', gem_path, 'install', 'ey-deploy', '-v', EYSD_VERSION]))
|
55
|
+
end
|
56
|
+
|
57
|
+
def upgrade_ey_deploy!
|
58
|
+
ssh "sudo #{gem_path} uninstall -a -x ey-deploy"
|
59
|
+
install_ey_deploy!
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def ssh(remote_command, output = true)
|
65
|
+
user = environment.username
|
66
|
+
|
67
|
+
cmd = Escape.shell_command(%w[ssh -o StrictHostKeyChecking=no -q] << "#{user}@#{hostname}" << remote_command)
|
68
|
+
cmd << " > /dev/null" unless output
|
69
|
+
output ? puts(cmd) : EY.ui.debug(cmd)
|
70
|
+
unless ENV["NO_SSH"]
|
71
|
+
system cmd
|
72
|
+
else
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def eysd_path
|
78
|
+
"/usr/local/ey_resin/ruby/bin/eysd"
|
79
|
+
end
|
80
|
+
|
81
|
+
def gem_path
|
82
|
+
"/usr/local/ey_resin/ruby/bin/gem"
|
83
|
+
end
|
84
|
+
|
85
|
+
def ruby_path
|
86
|
+
"/usr/local/ey_resin/ruby/bin/ruby"
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|