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
|
5 |
|
public static function fromDirectories(Exec $exec, Directories $directories) |
62
|
|
|
{ |
63
|
5 |
|
$packageDirectory = $directories->getBaseDirectory('XDG_CACHE_HOME', 'package-docker'); |
64
|
5 |
|
$binariesDirectory = $directories->getBaseDirectory('XDG_DATA_HOME', 'static-docker'); |
65
|
|
|
|
66
|
5 |
|
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
|
7 |
|
public function __construct(Exec $exec, $packageDirectory, $binariesDirectory) |
77
|
|
|
{ |
78
|
7 |
|
$this->exec = $exec; |
79
|
7 |
|
$this->packageDirectory = $packageDirectory; |
80
|
7 |
|
$this->binariesDirectory = $binariesDirectory; |
81
|
7 |
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Get binary path from local store. |
85
|
|
|
* |
86
|
|
|
* @param array $package |
87
|
|
|
* @return string |
88
|
|
|
*/ |
89
|
3 |
|
public function getLocalBinary(array $package) |
90
|
|
|
{ |
91
|
3 |
|
$package = $this->preparePackage($package); |
92
|
|
|
|
93
|
3 |
|
$binLocal = $package['prep']['bin_local']; |
94
|
3 |
|
$pkgLocal = $package['prep']['pkg_local']; |
95
|
|
|
|
96
|
3 |
|
if (!LibFs::isReadableFile($binLocal) && !LibFs::isReadableFile($pkgLocal)) { |
97
|
3 |
|
$this->download($package); |
98
|
|
|
} |
99
|
|
|
|
100
|
3 |
|
$this->extract($package); |
101
|
|
|
|
102
|
3 |
|
$message = sprintf('Verify and rename or remove the file to get it downloaded again from %s', $package['uri']); |
103
|
3 |
|
$this->verifyFileHash($binLocal, $package['binary_sha256'], $message); |
104
|
|
|
|
105
|
3 |
|
return $binLocal; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* @param array $package |
110
|
|
|
* @return array |
111
|
|
|
*/ |
112
|
3 |
|
public function preparePackage(array $package) |
113
|
|
|
{ |
114
|
3 |
|
$cache = libFs::mkDir($this->packageDirectory); |
115
|
3 |
|
$share = libFs::mkDir($this->binariesDirectory); |
116
|
|
|
|
117
|
3 |
|
$pkgBase = sprintf('%s/%s', $cache, basename($package['uri'])); |
118
|
3 |
|
$binBase = sprintf('%s/%s', $share, $package['name']); |
119
|
|
|
|
120
|
3 |
|
$package['prep'] = array( |
121
|
3 |
|
'cache' => $cache, |
122
|
3 |
|
'pkg_base' => $pkgBase, |
123
|
3 |
|
'pkg_local' => sprintf('%s.%s', $pkgBase, $package['sha256']), |
124
|
3 |
|
'share' => $share, |
125
|
3 |
|
'bin_base' => $binBase, |
126
|
3 |
|
'bin_local' => sprintf('%s.%s', $binBase, $package['binary_sha256']), |
127
|
|
|
); |
128
|
|
|
|
129
|
3 |
|
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
|
4 |
|
public function extractFromTgzFile($tgz, $path, $dest) |
138
|
|
|
{ |
139
|
4 |
|
if (!LibFs::isReadableFile($tgz)) { |
140
|
1 |
|
throw new \UnexpectedValueException(sprintf('Not a readable file: %s', $tgz)); |
141
|
|
|
} |
142
|
|
|
|
143
|
4 |
|
LibFs::rm($dest); |
144
|
4 |
|
$status = $this->exec->pass( |
145
|
4 |
|
sprintf('> %s tar', Lib::quoteArg($dest)), |
146
|
4 |
|
array('-xOzf', $tgz, $path) |
147
|
|
|
); |
148
|
|
|
|
149
|
4 |
|
if (0 !== $status) { |
150
|
1 |
|
LibFs::rm($dest); |
151
|
|
|
|
152
|
1 |
|
throw new \UnexpectedValueException(sprintf('Nonzero tar exit status: %d', $status)); |
153
|
|
|
} |
154
|
4 |
|
} |
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
|
4 |
|
public function verifyFileHash($file, $hash, $message = '') |
164
|
|
|
{ |
165
|
4 |
|
if (!LibFs::isReadableFile($file)) { |
166
|
1 |
|
throw new \UnexpectedValueException(sprintf('not a readable file: "%s"', $file)); |
167
|
|
|
} |
168
|
|
|
|
169
|
4 |
|
$actual = hash_file('sha256', $file); |
170
|
4 |
|
if ($actual !== $hash) { |
171
|
1 |
|
$buffer = sprintf( |
172
|
1 |
|
'sha256 checksum mismatch: "%s" for file "%s".', |
173
|
1 |
|
$hash, |
174
|
1 |
|
$file |
175
|
|
|
); |
176
|
1 |
|
strlen($message) && $buffer .= ' ' . $message; |
177
|
|
|
|
178
|
1 |
|
throw new \UnexpectedValueException($buffer); |
179
|
|
|
} |
180
|
4 |
|
} |
181
|
|
|
|
182
|
|
|
/** |
183
|
|
|
* @param array $package |
184
|
|
|
*/ |
185
|
3 |
|
private function download(array $package) |
186
|
|
|
{ |
187
|
3 |
|
$base = $package['prep']['pkg_base']; |
188
|
|
|
|
189
|
|
|
// Download the package if not in the http-cache |
190
|
3 |
|
$src = fopen($package['uri'], 'rb'); |
191
|
3 |
|
$dest = fopen(libFs::rm($base), 'wb'); |
192
|
3 |
|
$src && $dest && stream_copy_to_stream($src, $dest, self::BYTES_80MB); |
193
|
3 |
|
$src && fclose($src); |
194
|
3 |
|
$dest && fclose($dest); |
195
|
|
|
|
196
|
3 |
|
$hash = hash_file('sha256', $base); |
197
|
3 |
|
$pkgLocalIn = sprintf('%s.%s', $base, $hash); |
198
|
3 |
|
if (LibFs::IsReadableFile($pkgLocalIn)) { |
199
|
1 |
|
throw new \UnexpectedValueException(sprintf('Download collision: %s', $pkgLocalIn)); |
200
|
|
|
} |
201
|
3 |
|
LibFs::Rename($base, $pkgLocalIn); |
202
|
3 |
|
} |
203
|
|
|
|
204
|
3 |
|
private function extract(array $package) |
205
|
|
|
{ |
206
|
3 |
|
$binLocal = $package['prep']['bin_local']; |
207
|
|
|
|
208
|
3 |
|
if (LibFs::isReadableFile($binLocal)) { |
209
|
1 |
|
$this->verifyFileHash($binLocal, $package['binary_sha256']); |
210
|
|
|
|
211
|
1 |
|
return; |
212
|
|
|
} |
213
|
|
|
|
214
|
3 |
|
$pkgLocal = $package['prep']['pkg_local']; |
215
|
3 |
|
$base = $package['prep']['bin_base']; |
216
|
|
|
|
217
|
3 |
|
$this->extractFromTgzFile($pkgLocal, $package['binary'], $base); |
218
|
|
|
|
219
|
3 |
|
$hash = hash_file('sha256', $base); |
220
|
3 |
|
$binLocal = sprintf('%s.%s', $base, $hash); |
221
|
3 |
|
if (LibFs::isReadableFile($binLocal)) { |
222
|
1 |
|
LibFs::rm($base); |
223
|
|
|
|
224
|
1 |
|
throw new \UnexpectedValueException(sprintf('Extraction collision: "%s"', $binLocal)); |
225
|
|
|
} |
226
|
|
|
|
227
|
3 |
|
LibFs::Rename($base, $binLocal); |
228
|
3 |
|
} |
229
|
|
|
} |
230
|
|
|
|