|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
/* this file is part of pipelines */ |
|
4
|
|
|
|
|
5
|
|
|
namespace Ktomk\Pipelines\Runner\Docker\Binary; |
|
6
|
|
|
|
|
7
|
|
|
use Ktomk\Pipelines\Cli\Exec; |
|
8
|
|
|
use Ktomk\Pipelines\Lib; |
|
9
|
|
|
use Ktomk\Pipelines\LibFs; |
|
10
|
|
|
use Ktomk\Pipelines\Runner\Directories; |
|
11
|
|
|
|
|
12
|
|
|
/** |
|
13
|
|
|
* Class DockerBinaryUnpackager |
|
14
|
|
|
* |
|
15
|
|
|
* Knows how to handle a docker client binary package structure to provide |
|
16
|
|
|
* the docker client binary as pathname. |
|
17
|
|
|
* |
|
18
|
|
|
* Binaries are packaged in the docker project below https://download.docker.com/linux/static/stable/... |
|
19
|
|
|
* in versioned .tgz files (e.g. x86_64/docker-19.03.1.tgz). |
|
20
|
|
|
* |
|
21
|
|
|
* The docker binary is a single file inside such a .tgz file, which es getting extracted and then |
|
22
|
|
|
* is called the binary in the local store. |
|
23
|
|
|
* |
|
24
|
|
|
* Both the download of the .tgz file as well as the extraction of the docker binary from it is |
|
25
|
|
|
* verified against a checksum each so that downloading and extracting can be verified as successful. |
|
26
|
|
|
* |
|
27
|
|
|
* Binaries are stored in pipelines XDG_DATA_HOME in the static-docker folder, e.g. |
|
28
|
|
|
* ~/.local/share/pipelines/static-docker |
|
29
|
|
|
* |
|
30
|
|
|
* Before downloading, it is checked if the .tgz package is already cached. The cache is in the |
|
31
|
|
|
* XDG_CACHE_HOME, e.g. ~/.cache/pipelines/package-docker |
|
32
|
|
|
* |
|
33
|
|
|
* @package Ktomk\Pipelines\Runner\Docker |
|
34
|
|
|
*/ |
|
35
|
|
|
class UnPackager |
|
36
|
|
|
{ |
|
37
|
|
|
const BYTES_80MB = 83886080; |
|
38
|
|
|
|
|
39
|
|
|
/** |
|
40
|
|
|
* @var Exec |
|
41
|
|
|
*/ |
|
42
|
|
|
private $exec; |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* @var string |
|
46
|
|
|
*/ |
|
47
|
|
|
private $packageDirectory; |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* @var string |
|
51
|
|
|
*/ |
|
52
|
|
|
private $binariesDirectory; |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* Create binary unpackager based on default directories. |
|
56
|
|
|
* |
|
57
|
|
|
* @param Exec $exec |
|
58
|
|
|
* @param Directories $directories |
|
59
|
|
|
* @return UnPackager |
|
60
|
|
|
*/ |
|
61
|
|
|
public static function fromDirectories(Exec $exec, Directories $directories) |
|
62
|
|
|
{ |
|
63
|
|
|
$packageDirectory = $directories->getBaseDirectory('XDG_CACHE_HOME', 'package-docker'); |
|
64
|
|
|
$binariesDirectory = $directories->getBaseDirectory('XDG_DATA_HOME', 'static-docker'); |
|
65
|
|
|
|
|
66
|
|
|
return new self($exec, $packageDirectory, $binariesDirectory); |
|
67
|
|
|
} |
|
68
|
|
|
|
|
69
|
|
|
/** |
|
70
|
|
|
* BinaryUnPackager constructor. |
|
71
|
|
|
* |
|
72
|
|
|
* @param Exec $exec unpackager is using shell commands w/ tar for unpacking |
|
73
|
|
|
* @param string $packageDirectory where to store package downloads to (e.g. HTTP cache) |
|
74
|
|
|
* @param string $binariesDirectory where to store binaries to (can be kept ongoing to run pipelines) |
|
75
|
|
|
*/ |
|
76
|
|
|
public function __construct(Exec $exec, $packageDirectory, $binariesDirectory) |
|
77
|
|
|
{ |
|
78
|
|
|
$this->exec = $exec; |
|
79
|
|
|
$this->packageDirectory = $packageDirectory; |
|
80
|
|
|
$this->binariesDirectory = $binariesDirectory; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
/** |
|
84
|
|
|
* Get binary path from local store. |
|
85
|
|
|
* |
|
86
|
|
|
* @param array $package |
|
87
|
|
|
* @return string |
|
88
|
|
|
*/ |
|
89
|
|
|
public function getLocalBinary(array $package) |
|
90
|
|
|
{ |
|
91
|
|
|
$package = $this->preparePackage($package); |
|
92
|
|
|
|
|
93
|
|
|
$binLocal = $package['prep']['bin_local']; |
|
94
|
|
|
$pkgLocal = $package['prep']['pkg_local']; |
|
95
|
|
|
|
|
96
|
|
|
if (!LibFs::isReadableFile($binLocal) && !LibFs::isReadableFile($pkgLocal)) { |
|
97
|
|
|
$this->download($package); |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
$this->extract($package); |
|
101
|
|
|
|
|
102
|
|
|
$message = sprintf('Verify and rename or remove the file to get it downloaded again from %s', $package['uri']); |
|
103
|
|
|
$this->verifyFileHash($binLocal, $package['binary_sha256'], $message); |
|
104
|
|
|
|
|
105
|
|
|
return $binLocal; |
|
106
|
|
|
} |
|
107
|
|
|
|
|
108
|
|
|
/** |
|
109
|
|
|
* @param array $package |
|
110
|
|
|
* @return array |
|
111
|
|
|
*/ |
|
112
|
|
|
public function preparePackage(array $package) |
|
113
|
|
|
{ |
|
114
|
|
|
$cache = libFs::mkDir($this->packageDirectory); |
|
115
|
|
|
$share = libFs::mkDir($this->binariesDirectory); |
|
116
|
|
|
|
|
117
|
|
|
$pkgBase = sprintf('%s/%s', $cache, basename($package['uri'])); |
|
118
|
|
|
$binBase = sprintf('%s/%s', $share, $package['name']); |
|
119
|
|
|
|
|
120
|
|
|
$package['prep'] = array( |
|
121
|
|
|
'cache' => $cache, |
|
122
|
|
|
'pkg_base' => $pkgBase, |
|
123
|
|
|
'pkg_local' => sprintf('%s.%s', $pkgBase, $package['sha256']), |
|
124
|
|
|
'share' => $share, |
|
125
|
|
|
'bin_base' => $binBase, |
|
126
|
|
|
'bin_local' => sprintf('%s.%s', $binBase, $package['binary_sha256']), |
|
127
|
|
|
); |
|
128
|
|
|
|
|
129
|
|
|
return $package; |
|
130
|
|
|
} |
|
131
|
|
|
|
|
132
|
|
|
/** |
|
133
|
|
|
* @param string $tgz path to tar-gz (.tgz) file |
|
134
|
|
|
* @param string $path path in package to extract from |
|
135
|
|
|
* @param string $dest path to extract to |
|
136
|
|
|
*/ |
|
137
|
|
|
public function extractFromTgzFile($tgz, $path, $dest) |
|
138
|
|
|
{ |
|
139
|
|
|
if (!LibFs::isReadableFile($tgz)) { |
|
140
|
|
|
throw new \UnexpectedValueException(sprintf('Not a readable file: %s', $tgz)); |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
LibFs::rm($dest); |
|
144
|
|
|
$status = $this->exec->pass( |
|
145
|
|
|
sprintf('> %s tar', Lib::quoteArg($dest)), |
|
146
|
|
|
array('-xOzf', $tgz, $path) |
|
147
|
|
|
); |
|
148
|
|
|
|
|
149
|
|
|
if (0 !== $status) { |
|
150
|
|
|
LibFs::rm($dest); |
|
151
|
|
|
|
|
152
|
|
|
throw new \UnexpectedValueException(sprintf('Nonzero tar exit status: %d', $status)); |
|
153
|
|
|
} |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
/** |
|
157
|
|
|
* verify sha256 has of a file |
|
158
|
|
|
* |
|
159
|
|
|
* @param string $file |
|
160
|
|
|
* @param string $hash sha256 hash of the file |
|
161
|
|
|
* @param string $message [optional] additional error message information |
|
162
|
|
|
*/ |
|
163
|
|
|
public function verifyFileHash($file, $hash, $message = '') |
|
164
|
|
|
{ |
|
165
|
|
|
if (!LibFs::isReadableFile($file)) { |
|
166
|
|
|
throw new \UnexpectedValueException(sprintf('not a readable file: "%s"', $file)); |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
$actual = hash_file('sha256', $file); |
|
170
|
|
|
if ($actual !== $hash) { |
|
171
|
|
|
$buffer = sprintf( |
|
172
|
|
|
'sha256 checksum mismatch: "%s" for file "%s".', |
|
173
|
|
|
$hash, |
|
174
|
|
|
$file |
|
175
|
|
|
); |
|
176
|
|
|
strlen($message) && $buffer .= ' ' . $message; |
|
177
|
|
|
|
|
178
|
|
|
throw new \UnexpectedValueException($buffer); |
|
179
|
|
|
} |
|
180
|
|
|
} |
|
181
|
|
|
|
|
182
|
|
|
/** |
|
183
|
|
|
* @param array $package |
|
184
|
|
|
*/ |
|
185
|
|
|
private function download(array $package) |
|
186
|
|
|
{ |
|
187
|
|
|
$base = $package['prep']['pkg_base']; |
|
188
|
|
|
|
|
189
|
|
|
// Download the package if not in the http-cache |
|
190
|
|
|
stream_copy_to_stream( |
|
191
|
|
|
$src = fopen($package['uri'], 'rb'), |
|
|
|
|
|
|
192
|
|
|
$dest = fopen(libFs::rm($base), 'wb'), |
|
|
|
|
|
|
193
|
|
|
self::BYTES_80MB |
|
194
|
|
|
); |
|
195
|
|
|
fclose($src); |
|
|
|
|
|
|
196
|
|
|
fclose($dest); |
|
197
|
|
|
$hash = hash_file('sha256', $base); |
|
198
|
|
|
$pkgLocalIn = sprintf('%s.%s', $base, $hash); |
|
199
|
|
|
if (LibFs::IsReadableFile($pkgLocalIn)) { |
|
200
|
|
|
throw new \UnexpectedValueException(sprintf('Download collision: %s', $pkgLocalIn)); |
|
201
|
|
|
} |
|
202
|
|
|
LibFs::Rename($base, $pkgLocalIn); |
|
203
|
|
|
} |
|
204
|
|
|
|
|
205
|
|
|
private function extract(array $package) |
|
206
|
|
|
{ |
|
207
|
|
|
$binLocal = $package['prep']['bin_local']; |
|
208
|
|
|
|
|
209
|
|
|
if (LibFs::isReadableFile($binLocal)) { |
|
210
|
|
|
$this->verifyFileHash($binLocal, $package['binary_sha256']); |
|
211
|
|
|
|
|
212
|
|
|
return; |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
$pkgLocal = $package['prep']['pkg_local']; |
|
216
|
|
|
$base = $package['prep']['bin_base']; |
|
217
|
|
|
|
|
218
|
|
|
$this->extractFromTgzFile($pkgLocal, $package['binary'], $base); |
|
219
|
|
|
|
|
220
|
|
|
$hash = hash_file('sha256', $base); |
|
221
|
|
|
$binLocal = sprintf('%s.%s', $base, $hash); |
|
222
|
|
|
if (LibFs::isReadableFile($binLocal)) { |
|
223
|
|
|
LibFs::rm($base); |
|
224
|
|
|
|
|
225
|
|
|
throw new \UnexpectedValueException(sprintf('Extraction collision: "%s"', $binLocal)); |
|
226
|
|
|
} |
|
227
|
|
|
|
|
228
|
|
|
LibFs::Rename($base, $binLocal); |
|
229
|
|
|
} |
|
230
|
|
|
} |
|
231
|
|
|
|