Passed
Push — test ( ad6ada...f0eb37 )
by Tom
03:54
created

StepRunner::runNewContainer()   B

Complexity

Conditions 8
Paths 32

Size

Total Lines 52
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 8.1039

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 35
c 1
b 0
f 0
nc 32
nop 4
dl 0
loc 52
ccs 30
cts 34
cp 0.8824
crap 8.1039
rs 8.1155

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines\Runner;
6
7
use Ktomk\Pipelines\Cli\Docker;
8
use Ktomk\Pipelines\Cli\Exec;
9
use Ktomk\Pipelines\Cli\Streams;
10
use Ktomk\Pipelines\DestructibleString;
11
use Ktomk\Pipelines\File\Image;
12
use Ktomk\Pipelines\File\Step;
13
use Ktomk\Pipelines\Lib;
14
use Ktomk\Pipelines\LibFs;
15
use Ktomk\Pipelines\LibTmp;
16
use Ktomk\Pipelines\Runner\Docker\Binary\Repository;
0 ignored issues
show
Bug introduced by
The type Ktomk\Pipelines\Runner\Docker\Binary\Repository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
18
/**
19
 * Runner for a single step of a pipeline
20
 */
21
class StepRunner
22
{
23
    /**
24
     * @var RunOpts
25
     */
26
    private $runOpts;
27
28
    /**
29
     * @var Directories
30
     */
31
    private $directories;
32
33
    /**
34
     * @var Exec
35
     */
36
    private $exec;
37
38
    /**
39
     * @var Flags
40
     */
41
    private $flags;
42
43
    /**
44
     * @var Env
45
     */
46
    private $env;
47
48
    /**
49
     * @var Streams
50
     */
51
    private $streams;
52
53
    /**
54
     * list of temporary directory destructible markers
55
     *
56
     * @var array
57
     */
58
    private $temporaryDirectories = array();
59
60
    /**
61
     * DockerSession constructor.
62
     *
63
     * @param RunOpts $runOpts
64
     * @param Directories $directories source repository root directory based directories object
65
     * @param Exec $exec
66
     * @param Flags $flags
67
     * @param Env $env
68
     * @param Streams $streams
69
     */
70 13
    public function __construct(
71
        RunOpts $runOpts,
72
        Directories $directories,
73
        Exec $exec,
74
        Flags $flags,
75
        Env $env,
76
        Streams $streams
77
    )
78
    {
79 13
        $this->runOpts = $runOpts;
80 13
        $this->directories = $directories;
81 13
        $this->exec = $exec;
82 13
        $this->flags = $flags;
83 13
        $this->env = $env;
84 13
        $this->streams = $streams;
85 13
    }
86
87
    /**
88
     * @param Step $step
89
     * @return null|int exist status of step script or null if the run operation failed
90
     */
91 13
    public function runStep(Step $step)
92
    {
93 13
        $dir = $this->directories->getProjectDirectory();
94 13
        $env = $this->env;
95 13
        $exec = $this->exec;
96 13
        $streams = $this->streams;
97 13
        $reuseContainer = $this->flags->reuseContainer();
98 13
        $deployCopy = $this->flags->deployCopy();
99
100 13
        $name = $this->generateContainerName($step);
101
102 13
        if (false === $reuseContainer) {
103 10
            $this->zapContainerByName($name);
104
        }
105 13
        $image = $step->getImage();
106 13
        $env->setContainerName($name);
107
108
        # launch container
109 13
        $streams->out(sprintf(
110 13
            "\x1D+++ step #%d\n\n    name...........: %s\n    effective-image: %s\n    container......: %s\n",
111 13
            $step->getIndex() + 1,
112 13
            $step->getName() ? '"' . $step->getName() . '"' : '(unnamed)',
113 13
            $image->getName(),
114 13
            $name
115
        ));
116
117 13
        $id = null;
118 13
        if ($reuseContainer) {
119 3
            $id = $this->dockerGetContainerIdByName($name);
120
        }
121
122 13
        if (null === $id) {
123 11
            list($id, $status) = $this->runNewContainer($name, $dir, $deployCopy, $step);
124 11
            if (null === $id) {
125 1
                return $status;
126
            }
127
        }
128
129 12
        $streams->out(sprintf("    container-id...: %s\n\n", substr($id, 0, 12)));
130
131
        # TODO: different deployments, mount (default), mount-ro, copy
132 12
        if (null !== $result = $this->deployCopy($deployCopy, $id, $dir)) {
133 2
            return $result;
134
        }
135
136 10
        $status = $this->runStepScript($step, $streams, $exec, $name);
137
138 10
        $this->captureStepArtifacts($step, $deployCopy && 0 === $status, $id, $dir);
139
140 10
        $this->shutdownStepContainer($status, $id, $exec, $name);
141
142 10
        return $status;
143
    }
144
145
    /**
146
     * @param Step $step
147
     * @param bool $copy
148
     * @param string $id container id
149
     * @param string $dir to put artifacts in (project directory)
150
     * @throws \RuntimeException
151
     */
152 10
    private function captureStepArtifacts(Step $step, $copy, $id, $dir)
153
    {
154
        # capturing artifacts is only supported for deploy copy
155 10
        if (!$copy) {
156 5
            return;
157
        }
158
159 5
        $artifacts = $step->getArtifacts();
160
161 5
        if (null === $artifacts) {
162 2
            return;
163
        }
164
165 3
        $exec = $this->exec;
166 3
        $streams = $this->streams;
167
168 3
        $streams->out("\x1D+++ copying artifacts from container...\n");
169
170 3
        $source = new ArtifactSource($exec, $id, $dir);
171
172 3
        $patterns = $artifacts->getPatterns();
173 3
        foreach ($patterns as $pattern) {
174 3
            $this->captureArtifactPattern($source, $pattern, $dir);
175
        }
176
177 3
        $streams('');
178 3
    }
179
180
    /**
181
     * @see Runner::captureStepArtifacts()
182
     *
183
     * @param ArtifactSource $source
184
     * @param string $pattern
185
     * @param string $dir
186
     * @throws \RuntimeException
187
     */
188 3
    private function captureArtifactPattern(ArtifactSource $source, $pattern, $dir)
189
    {
190 3
        $exec = $this->exec;
191 3
        $streams = $this->streams;
192
193 3
        $id = $source->getId();
194 3
        $paths = $source->findByPattern($pattern);
195 3
        if (empty($paths)) {
196 1
            return;
197
        }
198
199 2
        $chunks = Lib::arrayChunkByStringLength($paths, 131072, 4);
200
201 2
        foreach ($chunks as $paths) {
202 2
            $docker = Lib::cmd('docker', array('exec', '-w', '/app', $id));
203 2
            $tar = Lib::cmd('tar', array('c', '-f', '-', $paths));
204 2
            $unTar = Lib::cmd('tar', array('x', '-f', '-', '-C', $dir));
205
206 2
            $command = $docker . ' ' . $tar . ' | ' . $unTar;
207 2
            $status = $exec->pass($command, array());
208
209 2
            if (0 !== $status) {
210 1
                $streams->err(sprintf(
211 1
                    "pipelines: Artifact failure: '%s' (%d, %d paths, %d bytes)\n",
212 1
                    $pattern,
213 1
                    $status,
214 1
                    count($paths),
215 1
                    strlen($command)
216
                ));
217
            }
218
        }
219 2
    }
220
221
    /**
222
     * @param bool $copy
223
     * @param string $id container id
224
     * @param string $dir directory to copy contents into container
225
     * @throws \RuntimeException
226
     * @return null|int null if all clear, integer for exit status
227
     */
228 12
    private function deployCopy($copy, $id, $dir)
229
    {
230 12
        if (!$copy) {
231 5
            return null;
232
        }
233
234 7
        $streams = $this->streams;
235 7
        $exec = $this->exec;
236
237 7
        $streams->out("\x1D+++ copying files into container...\n");
238
239 7
        $tmpDir = LibTmp::tmpDir('pipelines-cp.');
240 7
        $this->temporaryDirectories[] = DestructibleString::rmDir($tmpDir);
241 7
        LibFs::symlink($dir, $tmpDir . '/app');
242 7
        $cd = Lib::cmd('cd', array($tmpDir . '/.'));
243 7
        $tar = Lib::cmd('tar', array('c', '-h', '-f', '-', '--no-recursion', 'app'));
244 7
        $dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/.'));
245
        $status = $exec->pass("${cd} && echo 'app' | ${tar} | ${dockerCp}", array());
246
        LibFs::unlink($tmpDir . '/app');
247 7
        if (0 !== $status) {
248 1
            $streams->err('pipelines: deploy copy failure\n');
249
250 1
            return $status;
251
        }
252
253 6
        $cd = Lib::cmd('cd', array($dir . '/.'));
254 6
        $tar = Lib::cmd('tar', array('c', '-f', '-', '.'));
255 6
        $dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/app'));
256 6
        $status = $exec->pass("${cd} && ${tar} | ${dockerCp}", array());
257 6
        if (0 !== $status) {
258 1
            $streams->err('pipelines: deploy copy failure\n');
259
260 1
            return $status;
261
        }
262
263 5
        $streams('');
264
265 5
        return null;
266
    }
267
268
    /**
269
     * @param string $name
270
     * @return null|string
271
     */
272
    private function dockerGetContainerIdByName($name)
273
    {
274 3
        $ids = null;
275
276 3
        $status = $this->exec->capture(
277 3
            'docker',
278
            array(
279 3
                'ps', '-qa', '--filter',
280 3
                "name=^/${name}$"
281
            ),
282 3
            $result
283
        );
284
285 3
        $status || $ids = Lib::lines($result);
286
287 3
        if ($status || !(is_array($ids) && 1 === count($ids))) {
288 1
            return null;
289
        }
290
291 2
        return $ids[0];
292
    }
293
294
    /**
295
     * @param Step $step
296
     * @return string
297
     */
298
    private function generateContainerName(Step $step)
299
    {
300 13
        $project = $this->directories->getName();
301 13
        $idContainerSlug = preg_replace('([^a-zA-Z0-9_.-]+)', '-', $step->getPipeline()->getId());
302 13
        if ('' === $idContainerSlug) {
303 13
            $idContainerSlug = 'null';
304
        }
305 13
        $nameSlug = preg_replace(array('( )', '([^a-zA-Z0-9_.-]+)'), array('-', ''), $step->getName());
306 13
        if ('' === $nameSlug) {
307 13
            $nameSlug = 'no-name';
308
        }
309
310 13
        return $this->runOpts->getPrefix() . '-' . implode(
311 13
            '.',
312 13
            array_reverse(
313
                array(
314 13
                        $project,
315 13
                        trim($idContainerSlug, '-'),
316 13
                        $nameSlug,
317 13
                        $step->getIndex() + 1,
318
                    )
319
            )
320
        );
321
    }
322
323
    /**
324
     * @param Image $image
325
     * @throws \RuntimeException
326
     * @throws \InvalidArgumentException
327
     */
328
    private function imageLogin(Image $image)
329
    {
330 11
        $login = new DockerLogin($this->exec, $this->env->getResolver());
331 11
        $login->byImage($image);
332 11
    }
333
334
    /**
335
     * @param string $name
336
     * @param string $dir
337
     * @param bool $copy
338
     * @param Step $step
339
     * @return array array(string|null $id, int $status)
340
     */
341
    private function runNewContainer($name, $dir, $copy, Step $step)
342
    {
343 11
        $env = $this->env;
344 11
        $exec = $this->exec;
345 11
        $streams = $this->streams;
346
347 11
        $image = $step->getImage();
348
349
        # process docker login if image demands so, but continue on failure
350 11
        $this->imageLogin($image);
351
352
        // enable docker client inside docker by mounting docker socket
353
        // FIXME give controlling options, this is serious /!\
354 11
        $mountDockerSock = array();
355 11
        if ($this->flags->useDockerSocket() && file_exists('/var/run/docker.sock')) {
356
            $mountDockerSock = array(
357
                '-v', '/var/run/docker.sock:/var/run/docker.sock',
358
            );
359
        }
360
361 11
        $parentName = $env->getValue('PIPELINES_PARENT_CONTAINER_NAME');
362 11
        $checkMount = $mountDockerSock && null !== $parentName;
363 11
        $deviceDir = $dir;
364 11
        if ($checkMount) {
365
            $docker = new Docker($exec);
366
            $deviceDir = $docker->hostDevice($parentName, $dir);
367
            unset($docker);
368
        }
369
370 11
        $mountWorkingDirectory = $copy
371 7
            ? array()
372 11
            : array('--volume', "${deviceDir}:/app"); // FIXME(tk): hard encoded /app
373
374 11
        $status = $exec->capture('docker', array(
375 11
            'run', '-i', '--name', $name,
376 11
            $env->getArgs('-e'),
377 11
            $mountWorkingDirectory, '-e', 'BITBUCKET_CLONE_DIR=/app',
378 11
            $mountDockerSock,
379 11
            '--workdir', '/app', '--detach', '--entrypoint=/bin/sh', $image->getName()
380 11
        ), $out, $err);
381 11
        if (0 !== $status) {
382 1
            $streams->out("    container-id...: *failure*\n\n");
383 1
            $streams->err("pipelines: setting up the container failed\n");
384 1
            $streams->err("${err}\n");
385 1
            $streams->out("${out}\n");
386 1
            $streams->out(sprintf("exit status: %d\n", $status));
387
388 1
            return array(null, $status);
389
        }
390 10
        $id = rtrim($out) ?: '*dry-run*'; # side-effect: internal exploit of no output with true exit status
391
392 10
        return array($id, 0);
393
    }
394
395
    /**
396
     * @param Step $step
397
     * @param Streams $streams
398
     * @param Exec $exec
399
     * @param string $name container name
400
     * @return null|int should never be null, status, non-zero if a command failed
401
     */
402
    private function runStepScript(Step $step, Streams $streams, Exec $exec, $name)
403
    {
404 10
        $script = $step->getScript();
405
406 10
        $buffer = Lib::cmd("<<'SCRIPT' docker", array(
407 10
            'exec', '-i', $name, '/bin/sh'
408
        ));
409 10
        $buffer .= "\n# this /bin/sh script is generated from a pipelines pipeline:\n";
410 10
        foreach ($script as $line => $command) {
411 10
            $buffer .= 'printf \'\\035+ %s\\n\' ' . Lib::quoteArg($command) . "\n";
412 10
            $buffer .= $command . "\n";
413 10
            $buffer .= 'ret=$?' . "\n";
414 10
            $buffer .= 'printf \'\\n\'' . "\n";
415 10
            $buffer .= 'if [ $ret -ne 0 ]; then exit $ret; fi' . "\n";
416
        }
417 10
        $buffer .= "SCRIPT\n";
418
419 10
        $status = $exec->pass($buffer, array());
420
421 10
        if (0 !== $status) {
422 2
            $streams->err(sprintf("script non-zero exit status: %d\n", $status));
423
        }
424
425 10
        return $status;
426
    }
427
428
    /**
429
     * @param int $status
430
     * @param string $id container id
431
     * @param Exec $exec
432
     * @param string $name container name
433
     * @throws \RuntimeException
434
     */
435
    private function shutdownStepContainer($status, $id, Exec $exec, $name)
436
    {
437 10
        $flags = $this->flags;
438
439
        # keep container on error
440 10
        if (0 !== $status && $flags->keepOnError()) {
441 2
            $this->streams->err(sprintf(
442 2
                "error, keeping container id %s\n",
443 2
                substr($id, 0, 12)
444
            ));
445
446 2
            return;
447
        }
448
449
        # keep or remove container
450 8
        if ($flags->killContainer()) {
451 7
            $exec->capture('docker', array('kill', $name));
452
        }
453
454 8
        if ($flags->removeContainer()) {
455 7
            $exec->capture('docker', array('rm', $name));
456
        }
457
458 8
        if ($flags->keep()) {
459 1
            $this->streams->out(sprintf(
460 1
                "keeping container id %s\n",
461 1
                substr($id, 0, 12)
462
            ));
463
        }
464 8
    }
465
466
    /**
467
     * @param string $name
468
     */
469
    private function zapContainerByName($name)
470
    {
471 10
        $ids = null;
472
473 10
        $status = $this->exec->capture(
474 10
            'docker',
475
            array(
476 10
                'ps', '-qa', '--filter',
477 10
                "name=^/${name}$"
478
            ),
479 10
            $result
480
        );
481
482 10
        $status || $ids = Lib::lines($result);
483
484 10
        if ($status || !(is_array($ids) && 1 === count($ids))) {
485 9
            return;
486
        }
487
488 1
        $this->exec->capture('docker', Lib::merge('kill', $ids));
489 1
        $this->exec->capture('docker', Lib::merge('rm', $ids));
490 1
    }
491
}
492