Passed
Push — test ( 15b27e...ad6ada )
by Tom
02:51
created

StepRunner   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 519
Duplicated Lines 0 %

Test Coverage

Coverage 98.18%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 224
c 1
b 0
f 0
dl 0
loc 519
ccs 216
cts 220
cp 0.9818
rs 5.04
wmc 57

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A captureStepArtifacts() 0 26 4
A deployCopy() 0 38 4
B runStep() 0 59 9
A getDockerBinaryRepository() 0 6 1
A captureArtifactPattern() 0 28 4
A imageLogin() 0 4 1
A shutdownStepContainer() 0 27 6
A deployDockerClient() 0 19 3
A generateContainerName() 0 20 3
A runStepScript() 0 24 3
B runNewContainer() 0 52 8
A dockerGetContainerIdByName() 0 20 5
A zapContainerByName() 0 21 5

How to fix   Complexity   

Complex Class

Complex classes like StepRunner often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StepRunner, and based on these observations, apply Extract Interface, too.

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