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

StepRunner   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Test Coverage

Coverage 98.03%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 207
c 1
b 0
f 0
dl 0
loc 469
ccs 199
cts 203
cp 0.9803
rs 7.44
wmc 52

12 Methods

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

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;
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