bard 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/bard/cli.rb CHANGED
@@ -1,17 +1,9 @@
1
1
  # this file gets loaded in the CLI context, not the Rails boot context
2
2
 
3
3
  require "thor"
4
- require "bard/git"
5
- require "bard/ci"
6
- require "bard/copy"
7
- require "bard/github"
8
- require "bard/ping"
9
4
  require "bard/config"
10
5
  require "bard/command"
11
- require "bard/provision"
12
6
  require "term/ansicolor"
13
- require "open3"
14
- require "uri"
15
7
 
16
8
  module Bard
17
9
  class CLI < Thor
@@ -19,244 +11,24 @@ module Bard
19
11
 
20
12
  class_option :verbose, type: :boolean, aliases: :v
21
13
 
22
- desc "data --from=production --to=local", "copy database and assets from from to to"
23
- option :from, default: "production"
24
- option :to, default: "local"
25
- def data
26
- from = config[options[:from]]
27
- to = config[options[:to]]
28
-
29
- if to.key == :production
30
- url = to.ping.first
31
- puts yellow "WARNING: You are about to push data to production, overwriting everything that is there!"
32
- answer = ask("If you really want to do this, please type in the full HTTPS url of the production server:")
33
- if answer != url
34
- puts red("!!! ") + "Failed! We expected #{url}. Is this really where you want to overwrite all the data?"
35
- exit 1
36
- end
37
- end
38
-
39
- puts "Dumping #{from.key} database to file..."
40
- from.run! "bin/rake db:dump"
41
-
42
- puts "Transfering file from #{from.key} to #{to.key}..."
43
- from.copy_file "db/data.sql.gz", to: to, verbose: true
44
-
45
- puts "Loading file into #{to.key} database..."
46
- to.run! "bin/rake db:load"
47
-
48
- config.data.each do |path|
49
- puts "Synchronizing files in #{path}..."
50
- from.copy_dir path, to: to, verbose: true
51
- end
52
- end
53
-
54
- desc "master_key --from=production --to=local", "copy master key from from to to"
55
- option :from, default: "production"
56
- option :to, default: "local"
57
- def master_key
58
- from = config[options[:from]]
59
- to = config[options[:to]]
60
- from.copy_file "config/master.key", to:
61
- end
62
-
63
- desc "stage [branch=HEAD]", "pushes current branch, and stages it"
64
- def stage branch=Git.current_branch
65
- unless config.servers.key?(:production)
66
- raise Thor::Error.new("`bard stage` is disabled until a production server is defined. Until then, please use `bard deploy` to deploy to the staging server.")
67
- end
68
-
69
- run! "git push -u origin #{branch}", verbose: true
70
- config[:staging].run! "git fetch && git checkout -f origin/#{branch} && bin/setup"
71
- puts green("Stage Succeeded")
72
-
73
- ping :staging
74
- end
75
-
76
- option :"skip-ci", type: :boolean
77
- option :"local-ci", type: :boolean
78
- desc "deploy [TO=production]", "checks that current branch is a ff with master, checks with ci, merges into master, deploys to target, and then deletes branch."
79
- def deploy to=:production
80
- branch = Git.current_branch
81
-
82
- if branch == "master"
83
- if !Git.up_to_date_with_remote?(branch)
84
- run! "git push origin #{branch}:#{branch}"
85
- end
86
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
87
-
88
- else
89
- run! "git fetch origin master:master"
90
-
91
- unless Git.fast_forward_merge?("origin/master", branch)
92
- puts "The master branch has advanced. Attempting rebase..."
93
- run! "git rebase origin/master"
94
- end
95
-
96
- run! "git push -f origin #{branch}:#{branch}"
97
-
98
- invoke :ci, [branch], options.slice("local-ci") unless options["skip-ci"]
99
-
100
- run! "git push origin #{branch}:master"
101
- run! "git fetch origin master:master"
102
- end
103
-
104
- if `git remote` =~ /\bgithub\b/
105
- run! "git push github"
106
- end
107
-
108
- config[to].run! "git pull origin master && bin/setup"
109
-
110
- puts green("Deploy Succeeded")
111
-
112
- if branch != "master"
113
- puts "Deleting branch: #{branch}"
114
- run! "git push --delete origin #{branch}"
115
-
116
- if branch == Git.current_branch
117
- run! "git checkout master"
118
- end
119
-
120
- run! "git branch -D #{branch}"
121
- end
122
-
123
- ping to
124
- end
125
-
126
- option :"local-ci", type: :boolean
127
- option :status, type: :boolean
128
- desc "ci [branch=HEAD]", "runs ci against BRANCH"
129
- def ci branch=Git.current_branch
130
- ci = CI.new(project_name, branch, local: options["local-ci"])
131
- if ci.exists?
132
- return puts ci.status if options["status"]
133
-
134
- puts "Continuous integration: starting build on #{branch}..."
135
-
136
- success = ci.run do |elapsed_time, last_time|
137
- if last_time
138
- percentage = (elapsed_time.to_f / last_time.to_f * 100).to_i
139
- output = " Estimated completion: #{percentage}%"
140
- else
141
- output = " No estimated completion time. Elapsed time: #{elapsed_time} sec"
142
- end
143
- print "\x08" * output.length
144
- print output
145
- $stdout.flush
146
- end
147
-
148
- if success
149
- puts
150
- puts "Continuous integration: success!"
151
- puts "Deploying..."
152
- else
153
- puts
154
- puts ci.last_response
155
- puts ci.console
156
- puts red("Automated tests failed!")
157
- exit 1
158
- end
159
-
160
- else
161
- puts red("No CI found for #{project_name}!")
162
- puts "Re-run with --skip-ci to bypass CI, if you absolutely must, and know what you're doing."
163
- exit 1
164
- end
165
- end
166
-
167
- desc "open [server=production]", "opens the url in the web browser."
168
- def open server=:production
169
- exec "xdg-open #{config[server].ping.first}"
170
- end
171
-
172
- option :home, type: :boolean
173
- desc "ssh [to=production]", "logs into the specified server via SSH"
174
- def ssh to=:production
175
- config[to].exec! "exec $SHELL -l", home: options[:home]
176
- end
177
-
178
- desc "install", "copies bin/setup and bin/ci scripts into current project."
179
- def install
180
- install_files_path = File.expand_path(File.join(__dir__, "../../install_files/*"))
181
- system "cp -R #{install_files_path} bin/"
182
- github_files_path = File.expand_path(File.join(__dir__, "../../install_files/.github"))
183
- system "cp -R #{github_files_path} ./"
184
- end
185
-
186
- desc "provision [ssh_url]", "takes an ssh url to a raw ubuntu 22.04 install, and readies it in the shape of :production"
187
- def provision ssh_url
188
- Provision.call(config, ssh_url.dup) # dup unfreezes the string for later mutation
189
- end
190
-
191
- desc "setup", "installs app in nginx"
192
- def setup
193
- path = "/etc/nginx/sites-available/#{project_name}"
194
- dest_path = path.sub("sites-available", "sites-enabled")
195
- server_name = case ENV["RAILS_ENV"]
196
- when "production"
197
- (config[:production].ping.map do |str|
198
- "*.#{URI.parse(str).host}"
199
- end + ["_"]).join(" ")
200
- when "staging" then "#{project_name}.botandrose.com"
201
- else "#{project_name}.localhost"
202
- end
203
-
204
- system "sudo tee #{path} >/dev/null <<-EOF
205
- server {
206
- listen 80;
207
- server_name #{server_name};
208
-
209
- root #{Dir.pwd}/public;
210
- passenger_enabled on;
211
-
212
- location ~* \\.(ico|css|js|gif|jp?g|png|webp) {
213
- access_log off;
214
- if (\\$request_filename ~ \"-[0-9a-f]{32}\\.\") {
215
- expires max;
216
- add_header Cache-Control public;
217
- }
218
- }
219
- gzip_static on;
220
- }
221
- EOF"
222
- system "sudo ln -sf #{path} #{dest_path}" if !File.exist?(dest_path)
223
- system "sudo service nginx restart"
224
- end
225
-
226
- desc "ping [server=production]", "hits the server over http to verify that its up."
227
- def ping server=:production
228
- server = config[server]
229
- down_urls = Bard::Ping.call(config[server])
230
- down_urls.each { |url| puts "#{url} is down!" }
231
- exit 1 if down_urls.any?
232
- end
233
-
234
- # HACK: we don't use Thor::Base#run, so its okay to stomp on it here
235
- original_verbose, $VERBOSE = $VERBOSE, nil
236
- Thor::THOR_RESERVED_WORDS -= ["run"]
237
- $VERBOSE = original_verbose
238
-
239
- desc "run <command>", "run the given command on production"
240
- def run *args
241
- server = config[:production]
242
- server.run! *args, verbose: true
243
- end
244
-
245
- desc "hurt <command>", "reruns a command until it fails"
246
- def hurt *args
247
- 1.upto(Float::INFINITY) do |count|
248
- puts "Running attempt #{count}"
249
- system *args
250
- unless $?.success?
251
- puts "Ran #{count-1} times before failing"
252
- break
253
- end
254
- end
255
- end
256
-
257
- desc "vim [branch=master]", "open all files that have changed since master"
258
- def vim branch="master"
259
- exec "vim -p `git diff #{branch} --name-only | grep -v sass$ | tac`"
14
+ {
15
+ data: "Data",
16
+ stage: "Stage",
17
+ deploy: "Deploy",
18
+ ci: "CI",
19
+ master_key: "MasterKey",
20
+ setup: "Setup",
21
+ run: "Run",
22
+ open: "Open",
23
+ ssh: "SSH",
24
+ install: "Install",
25
+ provision: "Provision",
26
+ ping: "Ping",
27
+ hurt: "Hurt",
28
+ vim: "Vim",
29
+ }.each do |command, klass|
30
+ require "bard/cli/#{command}"
31
+ include const_get(klass)
260
32
  end
261
33
 
262
34
  def self.exit_on_failure? = true
@@ -273,6 +45,9 @@ EOF"
273
45
 
274
46
  def run!(...)
275
47
  Bard::Command.run!(...)
48
+ rescue Bard::Command::Error => e
49
+ puts red("!!! ") + "Running command failed: #{yellow(e.message)}"
50
+ exit 1
276
51
  end
277
52
  end
278
53
  end
data/lib/bard/command.rb CHANGED
@@ -1,5 +1,9 @@
1
+ require "open3"
2
+
1
3
  module Bard
2
4
  class Command < Struct.new(:command, :on, :home)
5
+ class Error < RuntimeError; end
6
+
3
7
  def self.run! command, on: :local, home: false, verbose: false, quiet: false
4
8
  new(command, on, home).run! verbose:, quiet:
5
9
  end
@@ -14,9 +18,7 @@ module Bard
14
18
 
15
19
  def run! verbose: false, quiet: false
16
20
  if !run(verbose:, quiet:)
17
- raise "Running command failed: #{full_command}"
18
- # puts red("!!! ") + "Running command failed: #{yellow(command)}"
19
- # exit 1
21
+ raise Error.new(full_command)
20
22
  end
21
23
  end
22
24
 
@@ -49,7 +51,6 @@ module Bard
49
51
  end
50
52
 
51
53
  def remote_command quiet: false
52
- uri = on.ssh_uri
53
54
  ssh_key = on.ssh_key ? "-i #{on.ssh_key} " : ""
54
55
  cmd = command
55
56
  if on.env
@@ -58,10 +59,11 @@ module Bard
58
59
  unless home
59
60
  cmd = "cd #{on.path} && #{cmd}"
60
61
  end
61
- cmd = "ssh -tt #{ssh_key}#{"-p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} '#{cmd}'"
62
+ uri = on.ssh_uri
63
+ cmd = "ssh -tt #{ssh_key} -p#{uri.port} #{uri.user}@#{uri.host} '#{cmd}'"
62
64
  if on.gateway
63
65
  uri = on.ssh_uri(:gateway)
64
- cmd = "ssh -tt #{" -p#{uri.port} " if uri.port}#{uri.user}@#{uri.host} \"#{cmd}\""
66
+ cmd = "ssh -tt -p#{uri.port} #{uri.user}@#{uri.host} \"#{cmd}\""
65
67
  end
66
68
  cmd += " 2>&1" if quiet
67
69
  cmd
data/lib/bard/config.rb CHANGED
@@ -12,13 +12,6 @@ module Bard
12
12
  "./",
13
13
  false,
14
14
  ),
15
- theia: Server.new(
16
- project_name,
17
- :theia,
18
- "gubito@gubs.pagekite.me",
19
- "Sites/#{project_name}",
20
- false,
21
- ),
22
15
  gubs: Server.new(
23
16
  project_name,
24
17
  :gubs,
@@ -40,7 +33,7 @@ module Bard
40
33
  ),
41
34
  }
42
35
  if path && File.exist?(path)
43
- source = File.read(File.expand_path(path))
36
+ source = File.read(path)
44
37
  end
45
38
  if source
46
39
  instance_eval source
@@ -51,9 +44,7 @@ module Bard
51
44
 
52
45
  def server key, &block
53
46
  key = key.to_sym
54
- @servers[key] ||= Server.new(project_name, key)
55
- @servers[key].instance_eval &block if block_given?
56
- @servers[key]
47
+ @servers[key] = Server.define(project_name, key, &block)
57
48
  end
58
49
 
59
50
  def [] key
data/lib/bard/copy.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require "uri"
2
+ require "bard/command"
3
+
1
4
  module Bard
2
5
  class Copy < Struct.new(:path, :from, :to, :verbose)
3
6
  def self.file path, from:, to:, verbose: false
@@ -19,33 +22,20 @@ module Bard
19
22
  end
20
23
 
21
24
  def scp_using_local direction, server
22
- uri = URI.parse("ssh://#{server.gateway}")
23
- port = uri.port ? "-p#{uri.port}" : ""
24
- gateway = server.gateway ? "-oProxyCommand='ssh #{port} #{uri.user}@#{uri.host} -W %h:%p'" : ""
25
+ gateway = server.gateway ? "-oProxyCommand='ssh #{server.ssh_uri(:gateway)} -W %h:%p'" : ""
25
26
 
26
27
  ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
27
28
 
28
- uri = URI.parse("ssh://#{server.ssh}")
29
- port = uri.port ? "-P#{uri.port}" : ""
30
- from_and_to = [path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
31
-
29
+ from_and_to = [path, server.scp_uri(path)]
32
30
  from_and_to.reverse! if direction == :from
33
- command = "scp #{gateway} #{ssh_key} #{port} #{from_and_to.join(" ")}"
34
31
 
32
+ command = ["scp", gateway, ssh_key, *from_and_to].join(" ")
35
33
  Bard::Command.run! command, verbose: verbose
36
34
  end
37
35
 
38
36
  def scp_as_mediator
39
37
  raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
40
-
41
- from_uri = URI.parse("ssh://#{from.ssh}")
42
- from_str = "scp://#{from_uri.user}@#{from_uri.host}:#{from_uri.port || 22}/#{from.path}/#{path}"
43
-
44
- to_uri = URI.parse("ssh://#{to.ssh}")
45
- to_str = "scp://#{to_uri.user}@#{to_uri.host}:#{to_uri.port || 22}/#{to.path}/#{path}"
46
-
47
- command = "scp -o ForwardAgent=yes #{from_str} #{to_str}"
48
-
38
+ command = "scp -o ForwardAgent=yes #{from.scp_uri(path)} #{to.scp_uri(path)}"
49
39
  Bard::Command.run! command, verbose: verbose
50
40
  end
51
41
 
@@ -55,46 +45,31 @@ module Bard
55
45
  elsif to.key == :local
56
46
  rsync_using_local :from, from
57
47
  else
58
- rsync_as_mediator from, to
48
+ rsync_as_mediator
59
49
  end
60
50
  end
61
51
 
62
52
  def rsync_using_local direction, server
63
- uri = URI.parse("ssh://#{server.gateway}")
64
- port = uri.port ? "-p#{uri.port}" : ""
65
- gateway = server.gateway ? "-oProxyCommand=\"ssh #{port} #{uri.user}@#{uri.host} -W %h:%p\"" : ""
53
+ gateway = server.gateway ? "-oProxyCommand=\"ssh #{server.ssh_uri(:gateway)} -W %h:%p\"" : ""
66
54
 
67
55
  ssh_key = server.ssh_key ? "-i #{server.ssh_key}" : ""
68
- uri = URI.parse("ssh://#{server.ssh}")
69
- port = uri.port ? "-p#{uri.port}" : ""
70
- ssh = "-e'ssh #{ssh_key} #{port} #{gateway}'"
56
+ ssh = "-e'ssh #{gateway} -p#{server.ssh_uri.port || 22}'"
71
57
 
72
- dest_path = path.dup
73
- dest_path = "./#{dest_path}"
74
- from_and_to = [dest_path, "#{uri.user}@#{uri.host}:#{server.path}/#{path}"]
58
+ from_and_to = ["./#{path}", server.rsync_uri(path)]
75
59
  from_and_to.reverse! if direction == :from
76
60
  from_and_to[-1].sub! %r(/[^/]+$), '/'
77
61
 
78
62
  command = "rsync #{ssh} --delete --info=progress2 -az #{from_and_to.join(" ")}"
79
-
80
63
  Bard::Command.run! command, verbose: verbose
81
64
  end
82
65
 
83
- def rsync_as_mediator from, to
66
+ def rsync_as_mediator
84
67
  raise NotImplementedError if from.gateway || to.gateway || from.ssh_key || to.ssh_key
85
68
 
86
- dest_path = path.dup
87
- dest_path = "./#{dest_path}"
88
-
89
- from_uri = URI.parse("ssh://#{from.ssh}")
90
- from_str = "-p#{from_uri.port || 22} #{from_uri.user}@#{from_uri.host}"
91
-
92
- to_uri = URI.parse("ssh://#{to.ssh}")
93
- to_str = "#{to_uri.user}@#{to_uri.host}:#{to.path}/#{path}"
94
- to_str.sub! %r(/[^/]+$), '/'
95
-
96
- command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to_uri.port || 22} -o StrictHostKeyChecking=no\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
69
+ from_str = "-p#{from.ssh_uri.port || 22} #{from.ssh_uri.user}@#{from.ssh_uri.host}"
70
+ to_str = to.rsync_uri(path).sub(%r(/[^/]+$), '/')
97
71
 
72
+ command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to.ssh_uri.port || 22} -o StrictHostKeyChecking=no\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
98
73
  Bard::Command.run! command, verbose: verbose
99
74
  end
100
75
  end
data/lib/bard/git.rb CHANGED
@@ -8,10 +8,6 @@ module Bard
8
8
  ref.sub(/refs\/heads\//, '') # refs/heads/master ... we want "master"
9
9
  end
10
10
 
11
- def current_sha
12
- sha_of("HEAD")
13
- end
14
-
15
11
  def fast_forward_merge?(root, branch)
16
12
  root_head = sha_of(root)
17
13
  branch_head = sha_of(branch)
@@ -23,6 +19,8 @@ module Bard
23
19
  sha_of(branch) == sha_of("origin/#{branch}")
24
20
  end
25
21
 
22
+ private
23
+
26
24
  def sha_of ref
27
25
  `git rev-parse #{ref}`.chomp
28
26
  end
data/lib/bard/ping.rb CHANGED
@@ -9,7 +9,6 @@ module Bard
9
9
 
10
10
  def call
11
11
  server.ping.reject do |url|
12
- next true if url == false
13
12
  response = get_response_with_redirect(url) rescue nil
14
13
  response.is_a?(Net::HTTPSuccess)
15
14
  end
@@ -27,7 +27,7 @@ class Bard::Provision::SSH < Bard::Provision
27
27
  ssh_url << ":#{target_port}"
28
28
  puts " ✓"
29
29
  end
30
-
30
+
31
31
  private
32
32
 
33
33
  def target_port
@@ -3,17 +3,10 @@ module Bard
3
3
  def self.call(...) = new(...).call
4
4
 
5
5
  def call
6
- SSH.call(*values)
7
- User.call(*values)
8
- Apt.call(*values)
9
- MySQL.call(*values)
10
- Repo.call(*values)
11
- MasterKey.call(*values)
12
- RVM.call(*values)
13
- App.call(*values)
14
- Passenger.call(*values)
15
- Data.call(*values)
16
- HTTP.call(*values)
6
+ %w[SSH User Apt MySQL Repo MasterKey RVM App Passenger Data HTTP].each do |step|
7
+ require "bard/provision/#{step.downcase}"
8
+ self.class.const_get(step).call(*values)
9
+ end
17
10
  end
18
11
 
19
12
  private
@@ -28,15 +21,3 @@ module Bard
28
21
  end
29
22
  end
30
23
 
31
- require "bard/provision/ssh"
32
- require "bard/provision/user"
33
- require "bard/provision/apt"
34
- require "bard/provision/mysql"
35
- require "bard/provision/repo"
36
- require "bard/provision/master_key"
37
- require "bard/provision/rvm"
38
- require "bard/provision/app"
39
- require "bard/provision/passenger"
40
- require "bard/provision/data"
41
- require "bard/provision/http"
42
-
data/lib/bard/server.rb CHANGED
@@ -3,7 +3,13 @@ require "bard/command"
3
3
  require "bard/copy"
4
4
 
5
5
  module Bard
6
- class Server < Struct.new(:project_name, :key, :ssh, :path, :ping, :gateway, :ssh_key, :env, :provision)
6
+ class Server < Struct.new(:project_name, :key, :ssh, :path, :ping, :gateway, :ssh_key, :env)
7
+ def self.define project_name, key, &block
8
+ new(project_name, key).tap do |server|
9
+ server.instance_eval &block
10
+ end
11
+ end
12
+
7
13
  def self.setting *fields
8
14
  fields.each do |field|
9
15
  define_method field do |*args|
@@ -18,20 +24,19 @@ module Bard
18
24
  end
19
25
  end
20
26
 
21
- setting :ssh, :path, :ping, :gateway, :ssh_key, :env, :provision
27
+ setting :ssh, :path, :ping, :gateway, :ssh_key, :env
22
28
 
23
29
  def ping(*args)
24
30
  if args.length == 0
25
- (super() || [nil]).map(&method(:normalize_ping))
31
+ (super() || [nil]).map(&method(:normalize_ping)).flatten
26
32
  else
27
33
  self.ping = args
28
34
  end
29
35
  end
30
36
 
31
37
  private def normalize_ping value
32
- return value if value == false
33
- uri = URI.parse("ssh://#{ssh}")
34
- normalized = "https://#{uri.host}" # default if none specified
38
+ return [] if value == false
39
+ normalized = "https://#{ssh_uri.host}" # default if none specified
35
40
  if value =~ %r{^/}
36
41
  normalized += value
37
42
  elsif value.to_s.length > 0
@@ -55,7 +60,24 @@ module Bard
55
60
 
56
61
  def ssh_uri which=:ssh
57
62
  value = send(which)
58
- URI.parse("ssh://#{value}")
63
+ URI("ssh://#{value}")
64
+ end
65
+
66
+ def scp_uri file_path=nil
67
+ ssh_uri.dup.tap do |uri|
68
+ uri.scheme = "scp"
69
+ uri.path = "/#{path}"
70
+ uri.path += "/#{file_path}" if file_path
71
+ end
72
+ end
73
+
74
+ def rsync_uri file_path=nil
75
+ ssh_uri.dup.tap do |uri|
76
+ uri.scheme = nil
77
+ uri.port = nil
78
+ uri.path = ":#{path}"
79
+ uri.path += "/#{file_path}" if file_path
80
+ end.to_s[2..]
59
81
  end
60
82
 
61
83
  def with(attrs)
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
4
4
 
@@ -0,0 +1,10 @@
1
+ require "bard/ci"
2
+
3
+ describe Bard::CI do
4
+ subject { described_class.new("tracker", "master") }
5
+
6
+ describe "#exists?"
7
+ describe "#status"
8
+ describe "#console"
9
+ describe "#run"
10
+ end