packaging 0.99.2 → 0.99.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,56 +1,12 @@
1
- def sign_rpm(rpm, sign_flags = nil)
2
-
3
- # To enable support for wrappers around rpm and thus support for gpg-agent
4
- # rpm signing, we have to be able to tell the packaging repo what binary to
5
- # use as the rpm signing tool.
6
- #
7
- rpm_cmd = ENV['RPM'] || Pkg::Util::Tool.find_tool('rpm')
8
-
9
- # If we're using the gpg agent for rpm signing, we don't want to specify the
10
- # input for the passphrase, which is what '--passphrase-fd 3' does. However,
11
- # if we're not using the gpg agent, this is required, and is part of the
12
- # defaults on modern rpm. The fun part of gpg-agent signing of rpms is
13
- # specifying that the gpg check command always return true
14
- #
15
- if Pkg::Util.boolean_value(ENV['RPM_GPG_AGENT'])
16
- gpg_check_cmd = "--define '%__gpg_check_password_cmd /bin/true'"
17
- else
18
- input_flag = "--passphrase-fd 3"
19
- end
20
-
21
- # Try this up to 5 times, to allow for incorrect passwords
22
- Pkg::Util::Execution.retry_on_fail(:times => 5) do
23
- # This definition of %__gpg_sign_cmd is the default on modern rpm. We
24
- # accept extra flags to override certain signing behavior for older
25
- # versions of rpm, e.g. specifying V3 signatures instead of V4.
26
- #
27
- sh "#{rpm_cmd} #{gpg_check_cmd} --define '%_gpg_name #{Pkg::Util::Gpg.key}' --define '%__gpg_sign_cmd %{__gpg} gpg #{sign_flags} #{input_flag} --batch --no-verbose --no-armor --no-secmem-warning -u %{_gpg_name} -sbo %{__signature_filename} %{__plaintext_filename}' --addsign #{rpm}"
28
- end
29
-
30
- end
31
-
32
- def sign_legacy_rpm(rpm)
33
- sign_rpm(rpm, "--force-v3-sigs --digest-algo=sha1")
34
- end
35
-
36
- def rpm_has_sig(rpm)
37
- %x(rpm -Kv #{rpm} | grep "#{Pkg::Util::Gpg.key.downcase}" &> /dev/null)
38
- $?.success?
39
- end
40
-
41
- def sign_deb_changes(file)
42
- # Lazy lazy lazy lazy lazy
43
- sign_program = "-p'gpg --use-agent --no-tty'" if ENV['RPM_GPG_AGENT']
44
- sh "debsign #{sign_program} --re-sign -k#{Pkg::Config.gpg_key} #{file}"
45
- end
46
-
47
1
  namespace :pl do
48
2
  desc "Sign the tarball, defaults to PL key, pass GPG_KEY to override or edit build_defaults"
49
3
  task :sign_tar do
50
4
  unless Pkg::Config.vanagon_project
51
- File.exist?("pkg/#{Pkg::Config.project}-#{Pkg::Config.version}.tar.gz") or fail "No tarball exists. Try rake package:tar?"
5
+ tarballs_to_sign = Pkg::Util::Ship.collect_packages(['pkg/*.tar.gz'], ['signing_bundle', 'packaging-bundle'])
52
6
  Pkg::Util::Gpg.load_keychain if Pkg::Util::Tool.find_tool('keychain')
53
- Pkg::Util::Gpg.sign_file "pkg/#{Pkg::Config.project}-#{Pkg::Config.version}.tar.gz"
7
+ tarballs_to_sign.each do |file|
8
+ Pkg::Util::Gpg.sign_file file
9
+ end
54
10
  end
55
11
  end
56
12
 
@@ -79,11 +35,27 @@ namespace :pl do
79
35
  task :sign_rpms, :root_dir do |t, args|
80
36
  rpm_dir = args.root_dir || "pkg"
81
37
 
82
- all_rpms = Dir["#{rpm_dir}/**/*.rpm"]
38
+ # Create a hash mapping full paths to basenames.
39
+ # This will allow us to keep track of the different paths that may be
40
+ # associated with a single basename, e.g. noarch packages.
41
+ all_rpms = {}
42
+ rpms_to_sign = Dir["#{rpm_dir}/**/*.rpm"]
43
+ rpms_to_sign.each do |rpm_path|
44
+ all_rpms[rpm_path] = File.basename(rpm_path)
45
+ end
46
+ # Delete a package, both from the signing server and from the rpm array, if
47
+ # there are other packages with the same basename so that we only sign the
48
+ # package once.
49
+ all_rpms.each do |rpm_path, rpm_filename|
50
+ if rpms_to_sign.map { |rpm| File.basename(rpm) }.count(rpm_filename) > 1
51
+ FileUtils.rm(rpm_path)
52
+ rpms_to_sign.delete(rpm_path)
53
+ end
54
+ end
83
55
 
84
56
  v3_rpms = []
85
57
  v4_rpms = []
86
- all_rpms.each do |rpm|
58
+ rpms_to_sign.each do |rpm|
87
59
  platform_tag = Pkg::Paths.tag_from_artifact_path(rpm)
88
60
  platform, version, _ = Pkg::Platforms.parse_platform_tag(platform_tag)
89
61
 
@@ -103,44 +75,38 @@ namespace :pl do
103
75
 
104
76
  unless v3_rpms.empty?
105
77
  puts "Signing old rpms..."
106
- sign_legacy_rpm(v3_rpms.join(' '))
78
+ Pkg::Sign::Rpm.legacy_sign(v3_rpms.join(' '))
107
79
  end
108
80
 
109
81
  unless v4_rpms.empty?
110
82
  puts "Signing modern rpms..."
111
- sign_rpm(v4_rpms.join(' '))
83
+ Pkg::Sign::Rpm.sign(v4_rpms.join(' '))
112
84
  end
113
85
 
114
- # Now we hardlink them back in
115
- Dir["#{rpm_dir}/**/*.noarch.rpm"].each do |rpm|
116
- platform_tag = Pkg::Paths.tag_from_artifact_path(rpm)
117
- platform, version, _ = Pkg::Platforms.parse_platform_tag(platform_tag)
118
- supported_arches = Pkg::Platforms.arches_for_platform_version(platform, version)
119
- cd File.dirname(rpm) do
120
- noarch_rpm = File.basename(rpm)
121
- supported_arches.each do |arch|
122
- arch_dir = File.join('..', arch)
123
- FileUtils.mkdir_p(arch_dir)
124
- unless File.exist?(File.join(arch_dir, noarch_rpm))
125
- FileUtils.ln(noarch_rpm, arch_dir, :force => true, :verbose => true)
126
- end
127
- end
86
+ # Using the map of paths to basenames, we re-hardlink the rpms we deleted.
87
+ all_rpms.each do |link_path, rpm_filename|
88
+ next if File.exist? link_path
89
+ FileUtils.mkdir_p(File.dirname(link_path))
90
+ # Find paths where the signed rpm has the same basename, but different
91
+ # full path, as the one we need to link.
92
+ paths_to_link_to = rpms_to_sign.select { |rpm| File.basename(rpm) == rpm_filename && rpm != link_path }
93
+ paths_to_link_to.each do |path|
94
+ FileUtils.ln(path, link_path, :force => true, :verbose => true)
128
95
  end
129
96
  end
130
97
  end
131
98
 
132
99
  desc "Sign ips package, uses PL certificates by default, update privatekey_pem, certificate_pem, and ips_inter_cert in build_defaults.yaml to override."
133
100
  task :sign_ips do
134
- Pkg::IPS.sign unless Dir['pkg/**/*.p5p'].empty?
101
+ Pkg::Sign::Ips.sign unless Dir['pkg/**/*.p5p'].empty?
135
102
  end
136
103
 
137
- if Pkg::Config.build_gem
138
- desc "Sign built gems, defaults to PL key, pass GPG_KEY to override or edit build_defaults"
139
- task :sign_gem do
140
- FileList["pkg/#{Pkg::Config.gem_name}-#{Pkg::Config.gemversion}*.gem"].each do |gem|
141
- puts "signing gem #{gem}"
142
- Pkg::Util::Gpg.sign_file(gem)
143
- end
104
+ desc "Sign built gems, defaults to PL key, pass GPG_KEY to override or edit build_defaults"
105
+ task :sign_gem do
106
+ gems = FileList["pkg/*.gem"]
107
+ gems.each do |gem|
108
+ puts "signing gem #{gem}"
109
+ Pkg::Util::Gpg.sign_file(gem)
144
110
  end
145
111
  end
146
112
 
@@ -150,7 +116,7 @@ namespace :pl do
150
116
  rpms = Dir["pkg/**/*.rpm"]
151
117
  print 'Checking rpm signatures'
152
118
  rpms.each do |rpm|
153
- if rpm_has_sig rpm
119
+ if Pkg::Sign::Rpm.has_sig? rpm
154
120
  print '.'
155
121
  else
156
122
  puts "#{rpm} is unsigned."
@@ -167,7 +133,7 @@ namespace :pl do
167
133
  change_files = Dir["pkg/**/*.changes"]
168
134
  unless change_files.empty?
169
135
  Pkg::Util::Gpg.load_keychain if Pkg::Util::Tool.find_tool('keychain')
170
- sign_deb_changes("pkg/**/*.changes")
136
+ Pkg::Sign::Deb.sign_changes("pkg/**/*.changes")
171
137
  end
172
138
  ensure
173
139
  Pkg::Util::Gpg.kill_keychain
@@ -176,12 +142,12 @@ namespace :pl do
176
142
 
177
143
  desc "Sign OSX packages"
178
144
  task :sign_osx => "pl:fetch" do
179
- Pkg::OSX.sign unless Dir['pkg/**/*.dmg'].empty?
145
+ Pkg::Sign::Dmg.sign unless Dir['pkg/**/*.dmg'].empty?
180
146
  end
181
147
 
182
148
  desc "Sign MSI packages"
183
149
  task :sign_msi => "pl:fetch" do
184
- Pkg::MSI.sign unless Dir['pkg/**/*.msi'].empty?
150
+ Pkg::Sign::Msi.sign unless Dir['pkg/**/*.msi'].empty?
185
151
  end
186
152
 
187
153
  ##
@@ -205,7 +171,8 @@ namespace :pl do
205
171
  signing_bundle = ENV['SIGNING_BUNDLE']
206
172
  rpm_sign_task = Pkg::Config.build_pe ? "pe:sign_rpms" : "pl:sign_rpms"
207
173
  deb_sign_task = Pkg::Config.build_pe ? "pe:sign_deb_changes" : "pl:sign_deb_changes"
208
- sign_tasks = [rpm_sign_task, deb_sign_task]
174
+ sign_tasks = [rpm_sign_task]
175
+ sign_tasks << deb_sign_task unless Dir['pkg/**/*.changes'].empty?
209
176
  sign_tasks << "pl:sign_tar" if Pkg::Config.build_tar
210
177
  sign_tasks << "pl:sign_gem" if Pkg::Config.build_gem
211
178
  sign_tasks << "pl:sign_osx" if Pkg::Config.build_dmg || Pkg::Config.vanagon_project
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: packaging
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.99.2
4
+ version: 0.99.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-14 00:00:00.000000000 Z
11
+ date: 2018-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -83,16 +83,19 @@ files:
83
83
  - lib/packaging/deb.rb
84
84
  - lib/packaging/deb/repo.rb
85
85
  - lib/packaging/gem.rb
86
- - lib/packaging/ips.rb
87
- - lib/packaging/msi.rb
88
86
  - lib/packaging/nuget.rb
89
- - lib/packaging/osx.rb
90
87
  - lib/packaging/paths.rb
91
88
  - lib/packaging/platforms.rb
92
89
  - lib/packaging/repo.rb
93
90
  - lib/packaging/retrieve.rb
94
91
  - lib/packaging/rpm.rb
95
92
  - lib/packaging/rpm/repo.rb
93
+ - lib/packaging/sign.rb
94
+ - lib/packaging/sign/deb.rb
95
+ - lib/packaging/sign/dmg.rb
96
+ - lib/packaging/sign/ips.rb
97
+ - lib/packaging/sign/msi.rb
98
+ - lib/packaging/sign/rpm.rb
96
99
  - lib/packaging/tar.rb
97
100
  - lib/packaging/util.rb
98
101
  - lib/packaging/util/date.rb
@@ -1,57 +0,0 @@
1
- module Pkg::IPS
2
- class << self
3
- def sign(target_dir = 'pkg')
4
- use_identity = "-i #{Pkg::Config.ips_signing_ssh_key}" unless Pkg::Config.ips_signing_ssh_key.nil?
5
-
6
- ssh_host_string = "#{use_identity} #{ENV['USER']}@#{Pkg::Config.ips_signing_server}"
7
- rsync_host_string = "-e 'ssh #{use_identity}' #{ENV['USER']}@#{Pkg::Config.ips_signing_server}"
8
-
9
- p5ps = Dir.glob("#{target_dir}/solaris/11/**/*.p5p")
10
-
11
- p5ps.each do |p5p|
12
- work_dir = "/tmp/#{Pkg::Util.rand_string}"
13
- unsigned_dir = "#{work_dir}/unsigned"
14
- repo_dir = "#{work_dir}/repo"
15
- signed_dir = "#{work_dir}/pkgs"
16
-
17
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "mkdir -p #{repo_dir} #{unsigned_dir} #{signed_dir}")
18
- Pkg::Util::Net.rsync_to(p5p, rsync_host_string, unsigned_dir)
19
-
20
- # Before we can get started with signing packages we need to create a repo
21
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "sudo -E /usr/bin/pkgrepo create #{repo_dir}")
22
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "sudo -E /usr/bin/pkgrepo set -s #{repo_dir} publisher/prefix=puppetlabs.com")
23
- # And import all the packages into the repo.
24
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "sudo -E /usr/bin/pkgrecv -s #{unsigned_dir}/#{File.basename(p5p)} -d #{repo_dir} '*'")
25
- # We are going to hard code the values for signing cert locations for now.
26
- # This autmation will require an update to actually become reusable, but
27
- # for now these values will stay this way so solaris signing will stop
28
- # failing. Please update soon. 06/23/16
29
- #
30
- # - Sean P. McDonald
31
- #
32
- # We sign the entire repo
33
- sign_cmd = "sudo -E /usr/bin/pkgsign -c /root/signing/signing_cert_interim_SHA1.pem \
34
- -i /root/signing/Thawte_Code_Signing_Certificate_interim_SHA1.pem \
35
- -i /root/signing/Thawte_Primary_Root_CA_interim_SHA1.pem \
36
- -k /root/signing/signing_key_interim_SHA1.pem \
37
- -s 'file://#{work_dir}/repo' '*'"
38
- puts "About to sign #{p5p} with #{sign_cmd} in #{work_dir}"
39
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, sign_cmd.squeeze(' '))
40
- # pkgrecv with -a will pull packages out of the repo, so we need to do that too to actually get the packages we signed
41
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "sudo -E /usr/bin/pkgrecv -d #{signed_dir}/#{File.basename(p5p)} -a -s #{repo_dir} '*'")
42
- begin
43
- # lets make sure we actually signed something?
44
- # **NOTE** if we're repeatedly trying to sign the same version this
45
- # might explode because I don't know how to reset the IPS cache.
46
- # Everything is amazing.
47
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "sudo -E /usr/bin/pkg contents -m -g #{signed_dir}/#{File.basename(p5p)} '*' | grep '^signature '")
48
- rescue RuntimeError
49
- raise "Looks like #{File.basename(p5p)} was not signed correctly, quitting!"
50
- end
51
- # and pull the packages back.
52
- Pkg::Util::Net.rsync_from("#{signed_dir}/#{File.basename(p5p)}", rsync_host_string, File.dirname(p5p))
53
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "if [ -e '#{work_dir}' ] ; then sudo rm -r '#{work_dir}' ; fi")
54
- end
55
- end
56
- end
57
- end
@@ -1,89 +0,0 @@
1
- module Pkg::MSI
2
- class << self
3
- def sign(target_dir = 'pkg')
4
- use_identity = "-i #{Pkg::Config.msi_signing_ssh_key}" if Pkg::Config.msi_signing_ssh_key
5
-
6
- ssh_host_string = "#{use_identity} Administrator@#{Pkg::Config.msi_signing_server}"
7
- rsync_host_string = "-e 'ssh #{use_identity}' Administrator@#{Pkg::Config.msi_signing_server}"
8
-
9
- work_dir = "Windows/Temp/#{Pkg::Util.rand_string}"
10
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "mkdir -p C:/#{work_dir}")
11
- msis = Dir.glob("#{target_dir}/windows/**/*.msi")
12
- Pkg::Util::Net.rsync_to(msis.join(" "), rsync_host_string, "/cygdrive/c/#{work_dir}")
13
-
14
- # Please Note:
15
- # We are currently adding two signatures to the msi.
16
- #
17
- # Microsoft compatable Signatures are composed of three different
18
- # elements.
19
- # 1) The Certificate used to sign the package. This is the element that
20
- # is attached to organization. The certificate has an associated
21
- # algorithm. We recently (February 2016) had to switch from a sha1 to
22
- # a sha256 certificate. Sha1 was deprecated by many Microsoft
23
- # elements on 2016-01-01, which forced us to switch to a sha256 cert.
24
- # This sha256 certificate is recognized by all currently supported
25
- # windows platforms (Windows 8/Vista forward).
26
- # 2) The signature used to attach the certificate to the package. This
27
- # can be a done with a variety of digest algorithms. Older platforms
28
- # (i.e., Windows 8 and Windows Vista) don't recognize later
29
- # algorithms like sha256.
30
- # 3) The timestamp used to validate when the package was signed. This
31
- # comes from an external source and can be delivered with a variety
32
- # of digest algorithms. Older platforms do not recognize newer
33
- # algorithms like sha256.
34
- #
35
- # We could have only one signature with the Sha256 Cert, Sha1 Signature,
36
- # and Sha1 Timestamp, but that would be too easy. The sha256 signature
37
- # and timestamp add more security to our packages. We can't have only
38
- # sha256 elements in our package signature, though, because Windows 8
39
- # and Windows Vista just don't recognize them at all.
40
- #
41
- # In order to add two signatures to an MSI, we also need to change the
42
- # tool we use to sign packages with. Previously, we were using SignTool
43
- # which is the Microsoft blessed program used to sign packages. However,
44
- # this tool isn't able to add two signatures to an MSI specifically. It
45
- # can dual-sign an exe, just not an MSI. In order to get the dual-signed
46
- # packages, we decided to switch over to using osslsigncode. The original
47
- # project didn't have support to compile on a windows system, so we
48
- # decided to use this fork. The binaries on the signer were pulled from
49
- # https://sourceforge.net/u/keeely/osslsigncode/ci/master/tree/
50
- #
51
- # These are our signatures:
52
- # The first signature:
53
- # * Sha256 Certificate
54
- # * Sha1 Signature
55
- # * Sha1 Timestamp
56
- #
57
- # The second signature:
58
- # * Sha256 Certificate
59
- # * Sha256 Signature
60
- # * Sha256 Timestamp
61
- #
62
- # Once we no longer support Windows 8/Windows Vista, we can remove the
63
- # first Sha1 signature.
64
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, %Q(for msi in #{msis.map { |d| File.basename(d) }.join(" ")}; do
65
- "/cygdrive/c/tools/osslsigncode-fork/osslsigncode.exe" sign \
66
- -n "Puppet" -i "http://www.puppet.com" \
67
- -h sha1 \
68
- -pkcs12 "#{Pkg::Config.msi_signing_cert}" \
69
- -pass "#{Pkg::Config.msi_signing_cert_pw}" \
70
- -t "http://timestamp.verisign.com/scripts/timstamp.dll" \
71
- -in "C:/#{work_dir}/$msi" \
72
- -out "C:/#{work_dir}/signed-$msi"
73
- "/cygdrive/c/tools/osslsigncode-fork/osslsigncode.exe" sign \
74
- -n "Puppet" -i "http://www.puppet.com" \
75
- -nest -h sha256 \
76
- -pkcs12 "#{Pkg::Config.msi_signing_cert}" \
77
- -pass "#{Pkg::Config.msi_signing_cert_pw}" \
78
- -ts "http://sha256timestamp.ws.symantec.com/sha256/timestamp" \
79
- -in "C:/#{work_dir}/signed-$msi" \
80
- -out "C:/#{work_dir}/$msi"
81
- rm "C:/#{work_dir}/signed-$msi"
82
- done))
83
- msis.each do | msi |
84
- Pkg::Util::Net.rsync_from("/cygdrive/c/#{work_dir}/#{File.basename(msi)}", rsync_host_string, File.dirname(msi))
85
- end
86
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "if [ -d '/cygdrive/c/#{work_dir}' ]; then rm -rf '/cygdrive/c/#{work_dir}'; fi")
87
- end
88
- end
89
- end
@@ -1,36 +0,0 @@
1
- module Pkg::OSX
2
- class << self
3
- def sign(target_dir = 'pkg')
4
- use_identity = "-i #{Pkg::Config.osx_signing_ssh_key}" unless Pkg::Config.osx_signing_ssh_key.nil?
5
-
6
- if Pkg::Config.osx_signing_server =~ /@/
7
- host_string = "#{Pkg::Config.osx_signing_server}"
8
- else
9
- host_string = "#{ENV['USER']}@#{Pkg::Config.osx_signing_server}"
10
- end
11
- ssh_host_string = "#{use_identity} #{host_string}"
12
- rsync_host_string = "-e 'ssh #{use_identity}' #{host_string}"
13
-
14
- work_dir = "/tmp/#{Pkg::Util.rand_string}"
15
- mount = File.join(work_dir, "mount")
16
- signed = File.join(work_dir, "signed")
17
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "mkdir -p #{mount} #{signed}")
18
- dmgs = Dir.glob("#{target_dir}/apple/**/*.dmg")
19
- Pkg::Util::Net.rsync_to(dmgs.join(" "), rsync_host_string, work_dir)
20
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, %Q[for dmg in #{dmgs.map { |d| File.basename(d, ".dmg") }.join(" ")}; do
21
- /usr/bin/hdiutil attach #{work_dir}/$dmg.dmg -mountpoint #{mount} -nobrowse -quiet ;
22
- /usr/bin/security -q unlock-keychain -p "#{Pkg::Config.osx_signing_keychain_pw}" "#{Pkg::Config.osx_signing_keychain}" ;
23
- for pkg in $(ls #{mount}/*.pkg | xargs -n 1 basename); do
24
- /usr/bin/productsign --keychain "#{Pkg::Config.osx_signing_keychain}" --sign "#{Pkg::Config.osx_signing_cert}" #{mount}/$pkg #{signed}/$pkg ;
25
- done
26
- /usr/bin/hdiutil detach #{mount} -quiet ;
27
- /bin/rm #{work_dir}/$dmg.dmg ;
28
- /usr/bin/hdiutil create -volname $dmg -srcfolder #{signed}/ #{work_dir}/$dmg.dmg ;
29
- /bin/rm #{signed}/* ; done])
30
- dmgs.each do | dmg |
31
- Pkg::Util::Net.rsync_from("#{work_dir}/#{File.basename(dmg)}", rsync_host_string, File.dirname(dmg))
32
- end
33
- Pkg::Util::Net.remote_ssh_cmd(ssh_host_string, "if [ -d '#{work_dir}' ]; then rm -rf '#{work_dir}'; fi")
34
- end
35
- end
36
- end