Passed
Push — master ( 572bc3...498a3b )
by Ronan
06:35 queued 01:29
created

AbstractProvider::processRefArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 0
cts 7
cp 0
rs 10
cc 2
nc 2
nop 1
crap 6
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 Closure;
11
use Exception;
12
use GuzzleHttp\ClientInterface;
13
use GuzzleHttp\Exception\ClientException;
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Message\StreamInterface;
16
use ReflectionClass;
17
use Ronanchilvers\Foundation\Config;
18
use Ronanchilvers\Utility\Str;
19
use RuntimeException;
20
use Symfony\Component\Process\Exception\ProcessFailedException;
21
use Symfony\Component\Process\Process;
22
use Symfony\Component\Yaml\Exception\ParseException;
23
use Symfony\Component\Yaml\Yaml;
24
25
/**
26
 * Base provider class
27
 *
28
 * @author Ronan Chilvers <[email protected]>
29
 */
30
abstract class AbstractProvider
31
{
32
    /**
33
     * @var ClientInterface
34
     */
35
    private $client;
36
37
    /**
38
     * @var string
39
     */
40
    protected $token;
41
42
    /**
43
     * @var array
44
     */
45
    protected $typesHandled = [];
46
47
    /**
48
     * @var string
49
     */
50
    protected $headUrl = null;
51
52
    /**
53
     * @var string
54
     */
55
    protected $branchAndTagUrl = null;
56
57
    /**
58
     * @var string
59
     */
60
    protected $commitUrl = null;
61
62
    /**
63
     * @var string
64
     */
65
    protected $downloadUrl = null;
66
67
    /**
68
     * @var string
69
     */
70
    protected $configUrl = null;
71
72
    /**
73
     * @var string
74
     */
75
    protected $repoUrl = null;
76
77
    /**
78
     * @var string
79
     */
80
    protected $branchUrl = null;
81
82
    /**
83
     * @var string
84
     */
85
    protected $shaUrl = null;
86
87
    /**
88
     * Class constructor
89
     *
90
     * @param string $token
91
     * @author Ronan Chilvers <[email protected]>
92
     */
93
    public function __construct(ClientInterface $client, string $token)
94
    {
95
        $this->client = $client;
96
        $this->token = $token;
97
    }
98
99
    /**
100
     * @see \App\Provider\ProviderInterface::handles()
101
     */
102
    public function handles(Project $project)
103
    {
104
        return in_array(
105
            $project->provider,
106
            $this->typesHandled
107
        );
108
    }
109
110
    /**
111
     * @author Ronan Chilvers <[email protected]>
112
     */
113
    public function getLabel()
114
    {
115
        $reflection = new ReflectionClass($this);
116
117
        return strtolower($reflection->getShortName());
118
    }
119
120
    /**
121
     * Get a repository link for a given repository
122
     *
123
     * @param string $repository
124
     * @return string
125
     * @author Ronan Chilvers <[email protected]>
126
     */
127
    public function getRepositoryLink(string $repository)
128
    {
129
        $params = [
130
            'repository' => $repository,
131
        ];
132
133
        return Str::moustaches(
134
            $this->repoUrl,
135
            $params
136
        );
137
    }
138
139
    /**
140
     * Get a link to a repository branch
141
     *
142
     * @param string $repository
143
     * @param string $branch
144
     * @return string
145
     * @author Ronan Chilvers <[email protected]>
146
     */
147
    public function getBranchLink(string $repository, string $branch)
148
    {
149
        $params = [
150
            'repository' => $repository,
151
            'branch'     => $branch,
152
        ];
153
154
        return Str::moustaches(
155
            $this->branchUrl,
156
            $params
157
        );
158
    }
159
160
    /**
161
     * Get a link for a given repository and sha
162
     *
163
     * @param string $repository
164
     * @param string $sha
165
     * @return string
166
     * @author Ronan Chilvers <[email protected]>
167
     */
168
    public function getShaLink(string $repository, string $sha)
169
    {
170
        $params = [
171
            'repository' => $repository,
172
            'sha'        => $sha,
173
        ];
174
175
        return Str::moustaches(
176
            $this->shaUrl,
177
            $params
178
        );
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     *
184
     * @return array
185
     * @author Ronan Chilvers <[email protected]>
186
     */
187
    public function getTagsAndBranches(string $repository)
188
    {
189
        $params = [
190
            'repository' => $this->encodeRepository($repository),
191
        ];
192
        $output = [];
193
194
        $url = Str::moustaches(
195
            $this->branchesUrl,
196
            $params
197
        );
198
        $branches = $this->getJSON($url);
199
        $branches = $this->processRefArray($branches);
200
        if (is_array($branches) && 0 < count($branches)) {
201
            $output['branch'] = $branches;
202
        }
203
204
        $url = Str::moustaches(
205
            $this->tagsUrl,
206
            $params
207
        );
208
        $tags = $this->getJSON($url);
209
        $tags = $this->processRefArray($tags);
210
        if (is_array($tags) && 0 < count($tags)) {
211
            $output['tag'] = $tags;
212
        }
213
214
        return $output;
215
    }
216
217
    /**
218
     * Process a ref arrays into simplified form
219
     *
220
     * @param array $data
221
     * @return array
222
     * @author Ronan Chilvers <[email protected]>
223
     */
224
    protected function processRefArray(array $data): array
225
    {
226
        $output = [];
227
        foreach ($data as $datum) {
228
            $output[$datum['name']] = $datum['name'];
229
        }
230
231
        return $output;
232
    }
233
234
    /**
235
     * @see \App\Provider\ProviderInterface::scanConfiguration()
236
     */
237
    public function scanConfiguration(Project $project, Deployment $deployment, Closure $closure = null)
238
    {
239
        try {
240
            $repository = $this->encodeRepository($project->repository);
241
            $params = [
242
                'repository' => $repository,
243
                'sha'        => $deployment->sha,
244
            ];
245
            $url = Str::moustaches(
246
                $this->configUrl,
247
                $params
248
            );
249
            $data = $this->getJSON($url);
250
            $yaml = base64_decode($data['content']);
251
            $yaml = Yaml::parse($yaml);
252
            $closure(
253
                'info',
254
                implode("\n", [
255
                    'YAML deployment configuration read successfully',
256
                    "JSON: " . json_encode($data, JSON_PRETTY_PRINT)
257
                ])
258
            );
259
260
            return new Config($yaml);
261
        } catch (ClientException $ex) {
262
            $closure(
263
                'info',
264
                implode("\n", [
265
                    'No deployment configuration found - using defaults',
266
                    "Exception: " . $ex->getMessage(),
267
                ])
268
            );
269
            Log::error('No deployment configuration found - using defaults', [
270
                'project'   => $project->toArray(),
271
                'exception' => $ex,
272
            ]);
273
274
            return;
275
        } catch (ParseException $ex) {
276
            $closure(
277
                'error',
278
                implode("\n", [
279
                    'Unable to parse YAML deployment configuration',
280
                    "Exception: " . $ex->getMessage(),
281
                ])
282
            );
283
            Log::error('Unable to parse YAML deployment configuration', [
284
                'project'   => $project->toArray(),
285
                'exception' => $ex,
286
            ]);
287
288
            throw $ex;
289
        }
290
    }
291
292
    /**
293
     * @see \App\Provider\ProviderInterface::download()
294
     */
295
    public function download(Project $project, Deployment $deployment, $directory, Closure $closure = null)
296
    {
297
        $repository = $this->encodeRepository($project->repository);
298
        $params = [
299
            'repository' => $repository,
300
            'sha'        => $deployment->sha,
301
        ];
302
        $url = Str::moustaches(
303
            $this->downloadUrl,
304
            $params
305
        );
306
        $filename = tempnam('/tmp', Str::join('-', 'deploy', $project->id, $params['sha']));
307
        if (!$handle = fopen($filename, "w")) {
308
            $closure(
309
                'error',
310
                implode("\n", [
311
                    "Unable to open temporary download file: {$filename}"
312
                ])
313
            );
314
            throw new RuntimeException('Unable to open temporary file');
315
        }
316
        $this->get(
317
            $url,
318
            [
319
                'sink' => $handle,
320
            ]
321
        );
322
323
        // Make sure the deployment download directory exists
324
        if (!is_dir($directory)) {
325
            $mode = Settings::get('build.chmod.default_folder', Builder::MODE_DEFAULT);
326
            $closure('info', "Creating deployment directory: {$directory}");
327
            if (!mkdir($directory, $mode, true)) {
328
                $closure('error', "Failed to create deployment directory: {$directory}");
329
                throw new RuntimeException(
330
                    'Unable to create build directory at ' . $directory
331
                );
332
            }
333
        }
334
335
        // Decompress the archive into the download directory
336
        $tar     = Settings::get('binary.tar', '/bin/tar');
337
        $command = "{$tar} --strip-components=1 -xzf {$filename} -C {$directory}";
338
        $closure('info', "Unpacking codebase tarball: {$command}");
339
        $process = new Process(explode(' ', $command));
340
        $process->run();
341
        if (!$process->isSuccessful()) {
342
            $closure(
343
                'error',
344
                implode("\n", [
345
                    "Unpack failed: {$command}",
346
                    $process->getErrorOutput(),
347
                ])
348
            );
349
            throw new ProcessFailedException($process);
350
        }
351
352
        // Remove the downloaded archive
353
        if (!unlink($filename)) {
354
            $closure(
355
                'error',
356
                implode("\n", [
357
                    'Unable to remove tarball after unpacking',
358
                    $process->getOutput(),
359
                ])
360
            );
361
            throw new RuntimeException('Unable to remove local code archive');
362
        }
363
364
        return true;
365
    }
366
367
    /**
368
     * Send a GET request to a URL and get back a JSON array
369
     *
370
     * @return array
371
     * @throws RuntimeException
372
     * @author Ronan Chilvers <[email protected]>
373
     */
374
    protected function getJSON($url, array $options = []): array
375
    {
376
        $response = $this->get($url, $options);
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
377
        $content = $respone->getBody()->getContents();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $respone does not exist. Did you maybe mean $response?
Loading history...
378
        Log::debug('Source control API response', [
379
            'provider' => get_called_class(),
380
            'body'     => $content,
381
        ]);
382
        if (empty($content)) {
383
            $content = '[]';
384
        }
385
        if (null === ($data = json_decode($content, true))) {
386
            throw new RuntimeException($this->getLabel() . ' : Invalid JSON response');
387
        }
388
        Log::debug('Source control JSON response', [
389
            'provider' => get_called_class(),
390
            'body'     => $content,
391
        ]);
392
393
        return $data;
394
    }
395
396
    /**
397
     * Send a GET request to a URL
398
     *
399
     * @return \Psr\Http\Message\ResponseInterface
400
     * @author Ronan Chilvers <[email protected]>
401
     */
402
    protected function get($url, array $options = []): ResponseInterface
403
    {
404
        return $this->client()->request(
405
            'GET',
406
            $url,
407
            $options
408
        );
409
    }
410
411
    /**
412
     * Get the HTTP client object
413
     *
414
     * @return \GuzzleHttp\ClientInterface
415
     * @author Ronan Chilvers <[email protected]>
416
     */
417
    protected function client(): ClientInterface
418
    {
419
        return $this->client;
420
    }
421
422
    /**
423
     * Encode a repository name
424
     *
425
     * @return string
426
     * @author Ronan Chilvers <[email protected]>
427
     */
428
    protected function encodeRepository($repository)
429
    {
430
        return $repository;
431
    }
432
}
433