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