Passed
Push — master ( a0c2c0...1d066e )
by Ronan
05:56 queued 02:14
created

AbstractProvider::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 Closure;
11
use Exception;
12
use GuzzleHttp\ClientInterface;
13
use Psr\Http\Message\ResponseInterface;
14
use Psr\Http\Message\StreamInterface;
15
use ReflectionClass;
16
use Ronanchilvers\Foundation\Config;
17
use Ronanchilvers\Utility\Str;
18
use RuntimeException;
19
use Symfony\Component\Process\Exception\ProcessFailedException;
20
use Symfony\Component\Process\Process;
21
use Symfony\Component\Yaml\Yaml;
22
23
/**
24
 * Base provider class
25
 *
26
 * @author Ronan Chilvers <[email protected]>
27
 */
28
abstract class AbstractProvider
29
{
30
    /**
31
     * @var ClientInterface
32
     */
33
    private $client;
34
35
    /**
36
     * @var string
37
     */
38
    protected $token;
39
40
    /**
41
     * @var array
42
     */
43
    protected $typesHandled = [];
44
45
    /**
46
     * @var string
47
     */
48
    protected $headUrl = null;
49
50
    /**
51
     * @var string
52
     */
53
    protected $commitUrl = null;
54
55
    /**
56
     * @var string
57
     */
58
    protected $downloadUrl = null;
59
60
    /**
61
     * @var string
62
     */
63
    protected $configUrl = null;
64
65
    /**
66
     * @var string
67
     */
68
    protected $repoUrl = null;
69
70
    /**
71
     * @var string
72
     */
73
    protected $branchUrl = null;
74
75
    /**
76
     * @var string
77
     */
78
    protected $shaUrl = null;
79
80
    /**
81
     * Class constructor
82
     *
83
     * @param string $token
84
     * @author Ronan Chilvers <[email protected]>
85
     */
86
    public function __construct(ClientInterface $client, string $token)
87
    {
88
        $this->client = $client;
89
        $this->token = $token;
90
    }
91
92
    /**
93
     * @see \App\Provider\ProviderInterface::handles()
94
     */
95
    public function handles(Project $project)
96
    {
97
        return in_array(
98
            $project->provider,
99
            $this->typesHandled
100
        );
101
    }
102
103
    /**
104
     * @author Ronan Chilvers <[email protected]>
105
     */
106
    public function getLabel()
107
    {
108
        $reflection = new ReflectionClass($this);
109
110
        return strtolower($reflection->getShortName());
111
    }
112
113
    /**
114
     * Get a repository link for a given repository
115
     *
116
     * @param string $repository
117
     * @return string
118
     * @author Ronan Chilvers <[email protected]>
119
     */
120
    public function getRepositoryLink(string $repository)
121
    {
122
        $params = [
123
            'repository' => $repository,
124
        ];
125
126
        return Str::moustaches(
127
            $this->repoUrl,
128
            $params
129
        );
130
    }
131
132
    /**
133
     * Get a link to a repository branch
134
     *
135
     * @param string $repository
136
     * @param string $branch
137
     * @return string
138
     * @author Ronan Chilvers <[email protected]>
139
     */
140
    public function getBranchLink(string $repository, string $branch)
141
    {
142
        $params = [
143
            'repository' => $repository,
144
            'branch'     => $branch,
145
        ];
146
147
        return Str::moustaches(
148
            $this->branchUrl,
149
            $params
150
        );
151
    }
152
153
    /**
154
     * Get a link for a given repository and sha
155
     *
156
     * @param string $repository
157
     * @param string $sha
158
     * @return string
159
     * @author Ronan Chilvers <[email protected]>
160
     */
161
    public function getShaLink(string $repository, string $sha)
162
    {
163
        $params = [
164
            'repository' => $repository,
165
            'sha'        => $sha,
166
        ];
167
168
        return Str::moustaches(
169
            $this->shaUrl,
170
            $params
171
        );
172
    }
173
174
    /**
175
     * @see \App\Provider\ProviderInterface::scanConfiguration()
176
     */
177
    public function scanConfiguration(Project $project, Deployment $deployment, Closure $closure = null)
178
    {
179
        $repository = $this->encodeRepository($project->repository);
180
        $params = [
181
            'repository' => $repository,
182
            'sha'        => $deployment->sha,
183
        ];
184
        $url = Str::moustaches(
185
            $this->configUrl,
186
            $params
187
        );
188
        $data = $this->getJSON($url);
189
        $yaml = base64_decode($data['content']);
190
        try {
191
            $yaml = Yaml::parse($yaml);
192
            $closure(
193
                'info',
194
                implode("\n", [
195
                    'YAML deployment configuration read successfully',
196
                    "JSON: " . json_encode($data, JSON_PRETTY_PRINT)
197
                ])
198
            );
199
        } catch (Exception $ex) {
200
            $closure(
201
                'error',
202
                implode("\n", [
203
                    'Unable to parse YAML deployment configuration',
204
                    "Exception: " . $ex->getMessage(),
205
                ])
206
            );
207
            Log::error('Unable to parse YAML deployment configuration', [
208
                'project'   => $project->toArray(),
209
                'exception' => $ex,
210
            ]);
211
            return;
212
        }
213
214
        return new Config($yaml);
215
    }
216
217
    /**
218
     * @see \App\Provider\ProviderInterface::download()
219
     */
220
    public function download(Project $project, Deployment $deployment, $directory, Closure $closure = null)
221
    {
222
        $repository = $this->encodeRepository($project->repository);
223
        $params = [
224
            'repository' => $repository,
225
            'sha'        => $deployment->sha,
226
        ];
227
        $url = Str::moustaches(
228
            $this->downloadUrl,
229
            $params
230
        );
231
        $filename = tempnam('/tmp', Str::join('-', 'deploy', $project->id, $params['sha']));
232
        if (!$handle = fopen($filename, "w")) {
233
            $closure(
234
                'error',
235
                implode("\n", [
236
                    "Unable to open temporary download file: {$filename}"
237
                ])
238
            );
239
            throw new RuntimeException('Unable to open temporary file');
240
        }
241
        $response = $this->get(
0 ignored issues
show
Unused Code introduced by
The assignment to $response is dead and can be removed.
Loading history...
242
            $url,
243
            [
244
                'sink' => $handle,
245
            ]
246
        );
247
248
        // Make sure the deployment download directory exists
249
        if (!is_dir($directory)) {
250
            $mode = Settings::get('build.chmod.default_folder', Builder::MODE_DEFAULT);
251
            $closure('info', "Creating deployment directory: {$directory}");
252
            if (!mkdir($directory, $mode, true)) {
253
                $closure('error', "Failed to create deployment directory: {$directory}");
254
                throw new RuntimeException(
255
                    'Unable to create build directory at ' . $directory
256
                );
257
            }
258
        }
259
260
        // Decompress the archive into the download directory
261
        $tar     = Settings::get('binary.tar', '/bin/tar');
262
        $command = "{$tar} --strip-components=1 -xzf {$filename} -C {$directory}";
263
        $closure('info', "Unpacking codebase tarball: {$command}");
264
        $process = new Process(explode(' ', $command));
265
        $process->run();
266
        if (!$process->isSuccessful()) {
267
            $closure(
268
                'error',
269
                implode("\n", [
270
                    "Unpack failed: {$command}",
271
                    $process->getErrorOutput(),
272
                ])
273
            );
274
            throw new ProcessFailedException($process);
275
        }
276
277
        // Remove the downloaded archive
278
        if (!unlink($filename)) {
279
            $closure(
280
                'error',
281
                implode("\n", [
282
                    'Unable to remove tarball after unpacking',
283
                    $process->getOutput(),
284
                ])
285
            );
286
            throw new RuntimeException('Unable to remove local code archive');
287
        }
288
289
        return true;
290
    }
291
292
    /**
293
     * Send a GET request to a URL and get back a JSON array
294
     *
295
     * @return array
296
     * @throws RuntimeException
297
     * @author Ronan Chilvers <[email protected]>
298
     */
299
    protected function getJSON($url, array $options = []): array
300
    {
301
        $response = $this->get($url, $options);
302
        $body = $response->getBody();
303
        if (!$body instanceof StreamInterface) {
0 ignored issues
show
introduced by
$body is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
304
            throw new RuntimeException($this->getLabel() . ' : Unable to read response body');
305
        }
306
        if (!$data = json_decode($body->getContents(), true)) {
307
            throw new RuntimeException($this->getLabel() . ' : Invalid JSON response for HEAD information request');
308
        }
309
310
        return $data;
311
    }
312
313
    /**
314
     * Send a GET request to a URL
315
     *
316
     * @return \Psr\Http\Message\ResponseInterface
317
     * @author Ronan Chilvers <[email protected]>
318
     */
319
    protected function get($url, array $options = []): ResponseInterface
320
    {
321
        return $this->client()->request(
322
            'GET',
323
            $url,
324
            $options
325
        );
326
    }
327
328
    /**
329
     * Get the HTTP client object
330
     *
331
     * @return \GuzzleHttp\ClientInterface
332
     * @author Ronan Chilvers <[email protected]>
333
     */
334
    protected function client(): ClientInterface
335
    {
336
        return $this->client;
337
    }
338
339
    /**
340
     * Encode a repository name
341
     *
342
     * @return string
343
     * @author Ronan Chilvers <[email protected]>
344
     */
345
    protected function encodeRepository($repository)
346
    {
347
        return $repository;
348
    }
349
}
350