Passed
Push — master ( 69b4a5...1004b6 )
by Ronan
06:49
created

Gitlab::encodeRepository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 0
cts 3
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace App\Provider;
4
5
use App\Builder;
6
use App\Facades\Log;
7
use App\Facades\Settings;
8
use App\Model\Deployment;
9
use App\Model\Project;
10
use App\Provider\ProviderInterface;
11
use Closure;
12
use Exception;
13
use Ronanchilvers\Foundation\Config;
14
use Ronanchilvers\Utility\Str;
15
use RuntimeException;
16
use Symfony\Component\Process\Exception\ProcessFailedException;
17
use Symfony\Component\Process\Process;
18
use Symfony\Component\Yaml\Yaml;
19
20
/**
21
 * Gitlab source control provider
22
 *
23
 * @author Ronan Chilvers <[email protected]>
24
 */
25
class Gitlab implements ProviderInterface
26
{
27
    /**
28
     * @var string
29
     */
30
    protected $token;
31
32
    /**
33
     * @var string
34
     */
35
    protected $headUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/commits/{branch}';
36
37
    /**
38
     * @var string
39
     */
40
    protected $downloadUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/archive.tar.gz?sha={sha}';
41
42
    /**
43
     * @var string
44
     */
45
    protected $configUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/files/deploy.yaml?ref={sha}';
46
47
    /**
48
     * @var string
49
     */
50
    protected $repoUrl = 'https://gitlab.com/{repository}';
51
52
    /**
53
     * @var string
54
     */
55
    protected $branchUrl = 'https://gitlab.com/{repository}/tree/{branch}';
56
57
    /**
58
     * @var string
59
     */
60
    protected $shaUrl = 'https://gitlab.com/{repository}/commit/{sha}';
61
62
    /**
63
     * Class constructor
64
     *
65
     * @param string $token
66
     * @author Ronan Chilvers <[email protected]>
67
     */
68
    public function __construct(string $token)
69
    {
70
        $this->token = $token;
71
    }
72
73
    /**
74
     * @author Ronan Chilvers <[email protected]>
75
     */
76
    public function getLabel()
77
    {
78
        return 'Gitlab';
79
    }
80
81
    /**
82
     * @see \App\Provider\ProviderInterface::handles()
83
     */
84
    public function handles(Project $project)
85
    {
86
        return 'gitlab' == $project->provider;
87
    }
88
89
    /**
90
     * Get a repository link for a given repository
91
     *
92
     * @param string $repository
93
     * @return string
94
     * @author Ronan Chilvers <[email protected]>
95
     */
96
    public function getRepositoryLink(string $repository)
97
    {
98
        $params = [
99
            'repository' => $repository,
100
        ];
101
102
        return Str::moustaches(
103
            $this->repoUrl,
104
            $params
105
        );
106
    }
107
108
    /**
109
     * Get a link to a repository branch
110
     *
111
     * @param string $repository
112
     * @param string $branch
113
     * @return string
114
     * @author Ronan Chilvers <[email protected]>
115
     */
116
    public function getBranchLink(string $repository, string $branch)
117
    {
118
        $params = [
119
            'repository' => $repository,
120
            'branch'     => $branch,
121
        ];
122
123
        return Str::moustaches(
124
            $this->branchUrl,
125
            $params
126
        );
127
    }
128
129
    /**
130
     * Get a link for a given repository and sha
131
     *
132
     * @param string $repository
133
     * @param string $sha
134
     * @return string
135
     * @author Ronan Chilvers <[email protected]>
136
     */
137
    public function getShaLink(string $repository, string $sha)
138
    {
139
        $params = [
140
            'repository' => $repository,
141
            'sha'        => $sha,
142
        ];
143
144
        return Str::moustaches(
145
            $this->shaUrl,
146
            $params
147
        );
148
    }
149
150
    /**
151
     * @see \App\Provider\ProviderInterface::getHeadInfo()
152
     */
153
    public function getHeadInfo(string $repository, string $branch, Closure $closure = null)
154
    {
155
        $params = [
156
            'repository' => $this->encodeRepository($repository),
157
            'branch'     => $branch,
158
        ];
159
        $url = Str::moustaches(
160
            $this->headUrl,
161
            $params
162
        );
163
        $closure('info', 'Querying Gitlab API for head commit data', "API URL : {$url}");
164
        $curl = $this->getCurlHandle($url);
165
        $data = curl_exec($curl);
166
        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
167
        curl_close($curl);
168
        if (200 != $statusCode) {
169
            $closure(
170
                'error',
171
                'Gitlab API request failed',
172
                implode("\n", [
173
                    "API URL - {$url}",
174
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl)
175
                ])
176
            );
177
            throw new RuntimeException('Unable to query Gitlab API - ' . $statusCode . ' response code');
178
        }
179
        if (!$data = json_decode($data, true)) {
180
            $closure(
181
                'error',
182
                'Unable to parse Gitlab response JSON',
183
                "API URL : {$url}"
184
            );
185
            throw new RuntimeException('Invalid commit data for head');
186
        }
187
188
        return [
189
            'sha'       => $data['id'],
190
            'author'    => $data['author_email'],
191
            'committer' => $data['committer_email'],
192
            'message'   => $data['title'],
193
        ];
194
    }
195
196
    /**
197
     * @see \App\Provider\ProviderInterface::download()
198
     */
199
    public function download(Project $project, Deployment $deployment, $directory, Closure $closure = null)
200
    {
201
        $params = [
202
            'repository' => $this->encodeRepository($project->repository),
203
            'sha'        => $deployment->sha,
204
        ];
205
        $url = Str::moustaches(
206
            $this->downloadUrl,
207
            $params
208
        );
209
        $closure(
210
            'info',
211
            'Initiating codebase download using Gitlab provider'
212
        );
213
214
        // Download the code tarball
215
        $filename = tempnam('/tmp', 'deploy-' . $params['sha'] . '-');
216
        if (!$handle = fopen($filename, "w")) {
217
            $closure(
218
                'error',
219
                'Unable to open temporary download file',
220
                implode("\n", [
221
                    "Temporary filename - {$filename}"
222
                ])
223
            );
224
            throw new RuntimeException('Unable to open temporary file');
225
        }
226
        $curl = $this->getCurlHandle($url);
227
        curl_setopt_array($curl, [
228
            CURLOPT_FOLLOWLOCATION => true,
229
            CURLOPT_FILE           => $handle
230
        ]);
231
        if (false === curl_exec($curl)) {
232
            $closure(
233
                'error',
234
                'Error downloading codebase',
235
                implode("\n", [
236
                    "Filename - {$filename}",
237
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl),
238
                ])
239
            );
240
            throw new RuntimeException(curl_errno($curl) . ' - ' . curl_error($curl));
241
        }
242
        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
243
        curl_close($curl);
244
        fclose($handle);
245
        if ($statusCode != 200) {
246
            $closure(
247
                'error',
248
                'Error downloading codebase',
249
                implode("\n", [
250
                    "Filename - {$filename}",
251
                    "Status code - {$statusCode}",
252
                ])
253
            );
254
            throw new RuntimeException('Failed to download codebase - ' . $statusCode);
255
        }
256
257
        // Make sure the deployment download directory exists
258
        if (!is_dir($directory)) {
259
            $mode = Settings::get('build.chmod.default_folder', Builder::MODE_DEFAULT);
260
            $closure(
261
                'info',
262
                'Creating deployment directory',
263
                implode("\n", [
264
                    "Directory - {$directory}",
265
                ])
266
            );
267
268
            if (!mkdir($directory, $mode, true)) {
269
                $closure(
270
                    'error',
271
                    'Failed to create deployment directory',
272
                    implode("\n", [
273
                        "Directory - {$directory}",
274
                    ])
275
                );
276
                throw new RuntimeException(
277
                    'Unable to create build directory at ' . $directory
278
                );
279
            }
280
        }
281
282
        // Decompress the archive into the download directory
283
        $tar     = Settings::get('binary.tar', '/bin/tar');
284
        $command = "{$tar} --strip-components=1 -xzf {$filename} -C {$directory}";
285
        $closure(
286
            'info',
287
            'Unpacking codebase tarball',
288
            implode("\n", [
289
                "Command - {$command}",
290
            ])
291
        );
292
        $process = new Process(explode(' ', $command));
293
        $process->run();
294
        if (!$process->isSuccessful()) {
295
            $closure(
296
                'error',
297
                'Failed to unpack codebase tarball',
298
                implode("\n", [
299
                    "Command - {$command}",
300
                    $process->getErrorOutput(),
301
                ])
302
            );
303
            throw new ProcessFailedException($process);
304
        }
305
306
        // Remove the downloaded archive
307
        if (!unlink($filename)) {
308
            $closure(
309
                'error',
310
                'Codebase tarball unpacked',
311
                implode("\n", [
312
                    $process->getOutput(),
313
                ])
314
            );
315
            throw new RuntimeException('Unable to remove local code archive');
316
        }
317
        $closure(
318
            'info',
319
            'Codebase download completed'
320
        );
321
322
        return true;
323
    }
324
325
    /**
326
     * @see \App\Provider\ProviderInterface::scanConfiguration()
327
     */
328
    public function scanConfiguration(Project $project, Deployment $deployment, Closure $closure = null)
329
    {
330
        $params = [
331
            'repository' => $this->encodeRepository($project->repository),
332
            'sha'        => $deployment->sha,
333
        ];
334
        $url = Str::moustaches(
335
            $this->configUrl,
336
            $params
337
        );
338
        $closure(
339
            'info',
340
            'Querying Gitlab API for deployment configuration',
341
            implode("\n", [
342
                "API URL - {$url}"
343
            ])
344
        );
345
        $curl = $this->getCurlHandle($url);
346
        $json = curl_exec($curl);
347
        if (false === $json || !is_string($json)) {
348
            $closure(
349
                'error',
350
                'Gitlab API request failed',
351
                implode("\n", [
352
                    "API URL - {$url}",
353
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl)
354
                ])
355
            );
356
            throw new RuntimeException("(" . curl_errno($curl) . ') ' . curl_error($curl));
357
        }
358
        $info = curl_getinfo($curl);
359
        if (404 == $info['http_code']) {
360
            $closure(
361
                'info',
362
                'No deployment configuration found in repository - using defaults'
363
            );
364
            Log::debug('Remote configuration file not found', [
365
                'project' => $project->toArray(),
366
            ]);
367
            return;
368
        }
369
        $data = json_decode($json, true);
370
        if (!$data || !isset($data['content'])) {
371
            $closure(
372
                'error',
373
                'Failed to parse Gitlab response json',
374
                implode("\n", [
375
                    "API URL - {$url}",
376
                    "JSON - " . $json
377
                ])
378
            );
379
            Log::debug('Remote configuration file could not be read', [
380
                'project' => $project->toArray(),
381
            ]);
382
            return;
383
        }
384
        $yaml = base64_decode($data['content']);
385
        try {
386
            $yaml = Yaml::parse($yaml);
387
            $closure(
388
                'info',
389
                'Parsed YAML deployment configuration successfully',
390
                implode("\n", [
391
                    "API URL - {$url}",
392
                    "JSON - " . $json
393
                ])
394
            );
395
        } catch (Exception $ex) {
396
            $closure(
397
                'info',
398
                'Unable to parse YAML deployment configuration',
399
                implode("\n", [
400
                    "API URL - {$url}",
401
                    "Exception - " . $ex->getMessage(),
402
                ])
403
            );
404
            Log::error('Unable to parse YAML deployment configuration', [
405
                'project'   => $project->toArray(),
406
                'exception' => $ex,
407
            ]);
408
            return;
409
        }
410
411
        return new Config($yaml);
412
    }
413
414
    /**
415
     * Get a curl handle
416
     *
417
     * @param string $url
418
     * @return resource
419
     * @author Ronan Chilvers <[email protected]>
420
     */
421
    protected function getCurlHandle($url)
422
    {
423
        if (!$curl = curl_init($url)) {
424
            throw new RuntimeException('Unable to initialise CURL Gitlab API request');
425
        }
426
        curl_setopt_array($curl, [
427
            CURLOPT_USERAGENT      => 'ronanchilvers/deploy - curl ' . curl_version()['version'],
428
            CURLOPT_FOLLOWLOCATION => false,
429
            CURLOPT_RETURNTRANSFER => true,
430
            CURLOPT_TIMEOUT        => 5,
431
            CURLOPT_HTTPHEADER     => [
432
                "Private-Token: {$this->token}"
433
            ],
434
        ]);
435
436
        return $curl;
437
    }
438
439
    /**
440
     * Encode a repository name
441
     *
442
     * @return string
443
     * @author Ronan Chilvers <[email protected]>
444
     */
445
    protected function encodeRepository($repository)
446
    {
447
        return str_replace('.', '%2E', urlencode($repository));
448
    }
449
}
450