Passed
Pull Request — master (#24)
by Ronan
09:51
created

Gitlab::getLabel()   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 0
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\AbstractProvider;
11
use App\Provider\ProviderInterface;
12
use Closure;
13
use Exception;
14
use Ronanchilvers\Foundation\Config;
15
use Ronanchilvers\Utility\Str;
16
use RuntimeException;
17
use Symfony\Component\Process\Exception\ProcessFailedException;
18
use Symfony\Component\Process\Process;
19
use Symfony\Component\Yaml\Yaml;
20
21
/**
22
 * Gitlab source control provider
23
 *
24
 * @author Ronan Chilvers <[email protected]>
25
 */
26
class Gitlab extends AbstractProvider implements ProviderInterface
27
{
28
    /**
29
     * @var array
30
     */
31
    protected $typesHandled = ['gitlab'];
32
33
    /**
34
     * @var string
35
     */
36
    protected $token;
37
38
    /**
39
     * @var string
40
     */
41
    protected $headUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/commits/{branch}';
42
43
    /**
44
     * @var string
45
     */
46
    protected $downloadUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/archive.tar.gz?sha={sha}';
47
48
    /**
49
     * @var string
50
     */
51
    protected $configUrl = 'https://gitlab.com/api/v4/projects/{repository}/repository/files/deploy.yaml?ref={sha}';
52
53
    /**
54
     * @var string
55
     */
56
    protected $repoUrl = 'https://gitlab.com/{repository}';
57
58
    /**
59
     * @var string
60
     */
61
    protected $branchUrl = 'https://gitlab.com/{repository}/tree/{branch}';
62
63
    /**
64
     * @var string
65
     */
66
    protected $shaUrl = 'https://gitlab.com/{repository}/commit/{sha}';
67
68
    /**
69
     * @see \App\Provider\ProviderInterface::getHeadInfo()
70
     */
71
    public function getHeadInfo(string $repository, string $branch)
72
    {
73
        $params = [
74
            'repository' => $this->encodeRepository($repository),
75
            'branch'     => $branch,
76
        ];
77
        $url = Str::moustaches(
78
            $this->headUrl,
79
            $params
80
        );
81
        $data = $this->getJSON($url);
82
83
        return [
84
            'sha'       => $data['id'],
85
            'author'    => $data['author_email'],
86
            'committer' => $data['committer_email'],
87
            'message'   => $data['title'],
88
        ];
89
    }
90
91
    /**
92
     * @see \App\Provider\ProviderInterface::download()
93
     */
94
    public function download(Project $project, Deployment $deployment, $directory, Closure $closure = null)
95
    {
96
        $params = [
97
            'repository' => $this->encodeRepository($project->repository),
98
            'sha'        => $deployment->sha,
99
        ];
100
        $url = Str::moustaches(
101
            $this->downloadUrl,
102
            $params
103
        );
104
        // $closure(
105
        //     'info',
106
        //     'Initiating codebase download using Gitlab provider'
107
        // );
108
109
        // Download the code tarball
110
        $filename = tempnam('/tmp', 'deploy-' . $params['sha'] . '-');
111
        if (!$handle = fopen($filename, "w")) {
112
            $closure('error', "Unable to open temporary download file: {$filename}");
113
            throw new RuntimeException('Unable to open temporary file');
114
        }
115
        $curl = $this->getCurlHandle($url);
116
        curl_setopt_array($curl, [
117
            CURLOPT_FOLLOWLOCATION => true,
118
            CURLOPT_FILE           => $handle
119
        ]);
120
        if (false === curl_exec($curl)) {
121
            $closure(
122
                'error',
123
                implode("\n", [
124
                    'Error downloading codebase',
125
                    "Filename - {$filename}",
126
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl),
127
                ])
128
            );
129
            throw new RuntimeException(curl_errno($curl) . ' - ' . curl_error($curl));
130
        }
131
        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
132
        curl_close($curl);
133
        fclose($handle);
134
        if ($statusCode != 200) {
135
            $closure(
136
                'error',
137
                implode("\n", [
138
                    'Error downloading codebase',
139
                    "Filename - {$filename}",
140
                    "Status code - {$statusCode}",
141
                ])
142
            );
143
            throw new RuntimeException('Failed to download codebase - ' . $statusCode);
144
        }
145
146
        // Make sure the deployment download directory exists
147
        if (!is_dir($directory)) {
148
            $mode = Settings::get('build.chmod.default_folder', Builder::MODE_DEFAULT);
149
            $closure('info', "Creating deployment directory: {$directory}");
150
151
            if (!mkdir($directory, $mode, true)) {
152
                $closure('error', "Failed to create deployment directory: {$directory}");
153
                throw new RuntimeException(
154
                    'Unable to create build directory at ' . $directory
155
                );
156
            }
157
        }
158
159
        // Decompress the archive into the download directory
160
        $tar     = Settings::get('binary.tar', '/bin/tar');
161
        $command = "{$tar} --strip-components=1 -xzf {$filename} -C {$directory}";
162
        $closure('info', "Unpacking codebase tarball: {$command}");
163
        $process = new Process(explode(' ', $command));
164
        $process->run();
165
        if (!$process->isSuccessful()) {
166
            $closure(
167
                'error',
168
                implode("\n", [
169
                    "Unpack failed: {$command}",
170
                    $process->getErrorOutput(),
171
                ])
172
            );
173
            throw new ProcessFailedException($process);
174
        }
175
176
        // Remove the downloaded archive
177
        if (!unlink($filename)) {
178
            $closure(
179
                'error',
180
                implode("\n", [
181
                    'Unable to remove tarball after unpacking',
182
                    $process->getOutput(),
183
                ])
184
            );
185
            throw new RuntimeException('Unable to remove local code archive');
186
        }
187
188
        return true;
189
    }
190
191
    /**
192
     * @see \App\Provider\ProviderInterface::scanConfiguration()
193
     */
194
    public function scanConfiguration(Project $project, Deployment $deployment, Closure $closure = null)
195
    {
196
        $params = [
197
            'repository' => $this->encodeRepository($project->repository),
198
            'sha'        => $deployment->sha,
199
        ];
200
        $url = Str::moustaches(
201
            $this->configUrl,
202
            $params
203
        );
204
        $closure('info', "Querying Gitlab API: {$url}");
205
        $curl = $this->getCurlHandle($url);
206
        $json = curl_exec($curl);
207
        if (false === $json || !is_string($json)) {
208
            $closure(
209
                'error',
210
                implode("\n", [
211
                    'Gitlab API request failed',
212
                    "API URL - {$url}",
213
                    "CURL Error - (" . curl_errno($curl) . ') ' . curl_error($curl)
214
                ])
215
            );
216
            throw new RuntimeException("(" . curl_errno($curl) . ') ' . curl_error($curl));
217
        }
218
        $info = curl_getinfo($curl);
219
        if (404 == $info['http_code']) {
220
            $closure(
221
                'info',
222
                'No deployment configuration found in repository - using defaults'
223
            );
224
            Log::debug('Remote configuration file not found', [
225
                'project' => $project->toArray(),
226
            ]);
227
            return;
228
        }
229
        $data = json_decode($json, true);
230
        if (!$data || !isset($data['content'])) {
231
            $closure(
232
                'error',
233
                implode("\n", [
234
                    'Failed to parse Gitlab response json',
235
                    "API URL - {$url}",
236
                    "JSON - " . $json
237
                ])
238
            );
239
            Log::debug('Remote configuration file could not be read', [
240
                'project' => $project->toArray(),
241
            ]);
242
            return;
243
        }
244
        $yaml = base64_decode($data['content']);
245
        try {
246
            $yaml = Yaml::parse($yaml);
247
            $closure(
248
                'info',
249
                implode("\n", [
250
                    'YAML deployment configuration read successfully',
251
                    "JSON - " . $json
252
                ])
253
            );
254
        } catch (Exception $ex) {
255
            $closure(
256
                'error',
257
                implode("\n", [
258
                    'Unable to parse YAML deployment configuration',
259
                    "Exception - " . $ex->getMessage(),
260
                ])
261
            );
262
            Log::error('Unable to parse YAML deployment configuration', [
263
                'project'   => $project->toArray(),
264
                'exception' => $ex,
265
            ]);
266
            return;
267
        }
268
269
        return new Config($yaml);
270
    }
271
272
    /**
273
     * Get a curl handle
274
     *
275
     * @param string $url
276
     * @return resource
277
     * @author Ronan Chilvers <[email protected]>
278
     */
279
    protected function getCurlHandle($url)
280
    {
281
        $curl = parent::getCurlHandle($url);
282
        curl_setopt_array($curl, [
283
            CURLOPT_HTTPHEADER     => [
284
                "Private-Token: {$this->token}"
285
            ],
286
        ]);
287
288
        return $curl;
289
    }
290
291
    /**
292
     * Encode a repository name
293
     *
294
     * @return string
295
     * @author Ronan Chilvers <[email protected]>
296
     */
297
    protected function encodeRepository($repository)
298
    {
299
        return str_replace('.', '%2E', urlencode($repository));
300
    }
301
}
302