Passed
Push — test ( 15b27e...ad6ada )
by Tom
02:51
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 17
    public function __construct(
71
        RunOpts $runOpts,
72
        Directories $directories,
73
        Exec $exec,
74
        Flags $flags,
75
        Env $env,
76
        Streams $streams
77
    )
78
    {
79 17
        $this->runOpts = $runOpts;
80 17
        $this->directories = $directories;
81 17
        $this->exec = $exec;
82 17
        $this->flags = $flags;
83 17
        $this->env = $env;
84 17
        $this->streams = $streams;
85 17
    }
86
87
    /**
88
     * @param Step $step
89
     * @return null|int exist status of step script or null if the run operation failed
90
     */
91 16
    public function runStep(Step $step)
92
    {
93 16
        $dir = $this->directories->getProjectDirectory();
94 16
        $env = $this->env;
95 16
        $exec = $this->exec;
96 16
        $streams = $this->streams;
97 16
        $reuseContainer = $this->flags->reuseContainer();
98 16
        $deployCopy = $this->flags->deployCopy();
99
100 16
        $name = $this->generateContainerName($step);
101
102 16
        if (false === $reuseContainer) {
103 13
            $this->zapContainerByName($name);
104
        }
105 16
        $image = $step->getImage();
106 16
        $env->setContainerName($name);
107
108
        # launch container
109 16
        $streams->out(sprintf(
110 16
            "\x1D+++ step #%d\n\n    name...........: %s\n    effective-image: %s\n    container......: %s\n",
111 16
            $step->getIndex() + 1,
112 16
            $step->getName() ? '"' . $step->getName() . '"' : '(unnamed)',
113 16
            $image->getName(),
114 16
            $name
115
        ));
116
117 16
        $id = null;
118 16
        if ($reuseContainer) {
119 3
            $id = $this->dockerGetContainerIdByName($name);
120
        }
121
122 16
        if (null === $id) {
123 14
            list($id, $status) = $this->runNewContainer($name, $dir, $deployCopy, $step);
124 14
            if (null === $id) {
125 1
                return $status;
126
            }
127
        }
128
129 15
        $streams->out(sprintf("    container-id...: %s\n\n", substr($id, 0, 12)));
130
131
        # TODO: different deployments, mount (default), mount-ro, copy
132 15
        if (null !== $result = $this->deployCopy($deployCopy, $id, $dir)) {
133 2
            return $result;
134
        }
135
136 13
        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 2
    public function getDockerBinaryRepository()
158
    {
159 2
        $repo = Repository::create($this->exec, $this->directories);
160 2
        $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 15
    private function deployCopy($copy, $id, $dir)
249
    {
250 15
        if (!$copy) {
251 8
            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
     * @throws
295
     * @return array array(int $status, string $message)
296
     */
297
    private function deployDockerClient(Step $step, $id)
298
    {
299 13
        if (!$step->getServices()->has('docker')) {
300 10
            return array(0, '');
301
        }
302
303 3
        $this->streams->out(' +++ docker client install...: ');
304
305
        try {
306 3
            list($status, $message) = $this->getDockerBinaryRepository()->inject($id);
307 1
        } catch (\Exception $e) {
308 1
            $this->streams->out("pipelines internal failure.\n");
309
310 1
            throw new \InvalidArgumentException('inject docker client failed: ' . $e->getMessage(), 1, $e);
311
        }
312
313 2
        $this->streams->out("${message}\n");
314
315 2
        return array($status, $message);
316
    }
317
318
    /**
319
     * @param string $name
320
     * @return null|string
321
     */
322
    private function dockerGetContainerIdByName($name)
323
    {
324 3
        $ids = null;
325
326 3
        $status = $this->exec->capture(
327 3
            'docker',
328
            array(
329 3
                'ps', '-qa', '--filter',
330 3
                "name=^/${name}$"
331
            ),
332 3
            $result
333
        );
334
335 3
        $status || $ids = Lib::lines($result);
336
337 3
        if ($status || !(is_array($ids) && 1 === count($ids))) {
338 1
            return null;
339
        }
340
341 2
        return $ids[0];
342
    }
343
344
    /**
345
     * @param Step $step
346
     * @return string
347
     */
348
    private function generateContainerName(Step $step)
349
    {
350 16
        $project = $this->directories->getName();
351 16
        $idContainerSlug = preg_replace('([^a-zA-Z0-9_.-]+)', '-', $step->getPipeline()->getId());
352 16
        if ('' === $idContainerSlug) {
353 16
            $idContainerSlug = 'null';
354
        }
355 16
        $nameSlug = preg_replace(array('( )', '([^a-zA-Z0-9_.-]+)'), array('-', ''), $step->getName());
356 16
        if ('' === $nameSlug) {
357 16
            $nameSlug = 'no-name';
358
        }
359
360 16
        return $this->runOpts->getPrefix() . '-' . implode(
361 16
            '.',
362 16
            array_reverse(
363
                array(
364 16
                        $project,
365 16
                        trim($idContainerSlug, '-'),
366 16
                        $nameSlug,
367 16
                        $step->getIndex() + 1,
368
                    )
369
            )
370
        );
371
    }
372
373
    /**
374
     * @param Image $image
375
     * @throws \RuntimeException
376
     * @throws \InvalidArgumentException
377
     */
378
    private function imageLogin(Image $image)
379
    {
380 14
        $login = new DockerLogin($this->exec, $this->env->getResolver());
381 14
        $login->byImage($image);
382 14
    }
383
384
    /**
385
     * @param string $name
386
     * @param string $dir
387
     * @param bool $copy
388
     * @param Step $step
389
     * @return array array(string|null $id, int $status)
390
     */
391
    private function runNewContainer($name, $dir, $copy, Step $step)
392
    {
393 14
        $env = $this->env;
394 14
        $exec = $this->exec;
395 14
        $streams = $this->streams;
396
397 14
        $image = $step->getImage();
398
399
        # process docker login if image demands so, but continue on failure
400 14
        $this->imageLogin($image);
401
402
        // enable docker client inside docker by mounting docker socket
403
        // FIXME give controlling options, this is serious /!\
404 14
        $mountDockerSock = array();
405 14
        if ($this->flags->useDockerSocket() && file_exists('/var/run/docker.sock')) {
406
            $mountDockerSock = array(
407
                '-v', '/var/run/docker.sock:/var/run/docker.sock',
408
            );
409
        }
410
411 14
        $parentName = $env->getValue('PIPELINES_PARENT_CONTAINER_NAME');
412 14
        $checkMount = $mountDockerSock && null !== $parentName;
413 14
        $deviceDir = $dir;
414 14
        if ($checkMount) {
415
            $docker = new Docker($exec);
416
            $deviceDir = $docker->hostDevice($parentName, $dir);
417
            unset($docker);
418
        }
419
420 14
        $mountWorkingDirectory = $copy
421 7
            ? array()
422 14
            : array('--volume', "${deviceDir}:/app"); // FIXME(tk): hard encoded /app
423
424 14
        $status = $exec->capture('docker', array(
425 14
            'run', '-i', '--name', $name,
426 14
            $env->getArgs('-e'),
427 14
            $mountWorkingDirectory, '-e', 'BITBUCKET_CLONE_DIR=/app',
428 14
            $mountDockerSock,
429 14
            '--workdir', '/app', '--detach', '--entrypoint=/bin/sh', $image->getName()
430 14
        ), $out, $err);
431 14
        if (0 !== $status) {
432 1
            $streams->out("    container-id...: *failure*\n\n");
433 1
            $streams->err("pipelines: setting up the container failed\n");
434 1
            $streams->err("${err}\n");
435 1
            $streams->out("${out}\n");
436 1
            $streams->out(sprintf("exit status: %d\n", $status));
437
438 1
            return array(null, $status);
439
        }
440 13
        $id = rtrim($out) ?: '*dry-run*'; # side-effect: internal exploit of no output with true exit status
441
442 13
        return array($id, 0);
443
    }
444
445
    /**
446
     * @param Step $step
447
     * @param Streams $streams
448
     * @param Exec $exec
449
     * @param string $name container name
450
     * @return null|int should never be null, status, non-zero if a command failed
451
     */
452
    private function runStepScript(Step $step, Streams $streams, Exec $exec, $name)
453
    {
454 11
        $script = $step->getScript();
455
456 11
        $buffer = Lib::cmd("<<'SCRIPT' docker", array(
457 11
            'exec', '-i', $name, '/bin/sh'
458
        ));
459 11
        $buffer .= "\n# this /bin/sh script is generated from a pipelines pipeline:\n";
460 11
        foreach ($script as $line => $command) {
461 11
            $buffer .= 'printf \'\\035+ %s\\n\' ' . Lib::quoteArg($command) . "\n";
462 11
            $buffer .= $command . "\n";
463 11
            $buffer .= 'ret=$?' . "\n";
464 11
            $buffer .= 'printf \'\\n\'' . "\n";
465 11
            $buffer .= 'if [ $ret -ne 0 ]; then exit $ret; fi' . "\n";
466
        }
467 11
        $buffer .= "SCRIPT\n";
468
469 11
        $status = $exec->pass($buffer, array());
470
471 11
        if (0 !== $status) {
472 2
            $streams->err(sprintf("script non-zero exit status: %d\n", $status));
473
        }
474
475 11
        return $status;
476
    }
477
478
    /**
479
     * @param int $status
480
     * @param string $id container id
481
     * @param Exec $exec
482
     * @param string $name container name
483
     * @throws \RuntimeException
484
     */
485
    private function shutdownStepContainer($status, $id, Exec $exec, $name)
486
    {
487 11
        $flags = $this->flags;
488
489
        # keep container on error
490 11
        if (0 !== $status && $flags->keepOnError()) {
491 2
            $this->streams->err(sprintf(
492 2
                "error, keeping container id %s\n",
493 2
                substr($id, 0, 12)
494
            ));
495
496 2
            return;
497
        }
498
499
        # keep or remove container
500 9
        if ($flags->killContainer()) {
501 8
            $exec->capture('docker', array('kill', $name));
502
        }
503
504 9
        if ($flags->removeContainer()) {
505 8
            $exec->capture('docker', array('rm', $name));
506
        }
507
508 9
        if ($flags->keep()) {
509 1
            $this->streams->out(sprintf(
510 1
                "keeping container id %s\n",
511 1
                substr($id, 0, 12)
512
            ));
513
        }
514 9
    }
515
516
    /**
517
     * @param string $name
518
     */
519
    private function zapContainerByName($name)
520
    {
521 13
        $ids = null;
522
523 13
        $status = $this->exec->capture(
524 13
            'docker',
525
            array(
526 13
                'ps', '-qa', '--filter',
527 13
                "name=^/${name}$"
528
            ),
529 13
            $result
530
        );
531
532 13
        $status || $ids = Lib::lines($result);
533
534 13
        if ($status || !(is_array($ids) && 1 === count($ids))) {
535 12
            return;
536
        }
537
538 1
        $this->exec->capture('docker', Lib::merge('kill', $ids));
539 1
        $this->exec->capture('docker', Lib::merge('rm', $ids));
540 1
    }
541
}
542