Passed
Push — master ( 1f6f72...a5203b )
by Tom
02:42
created

StepRunner   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 561
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 221
dl 0
loc 561
ccs 216
cts 216
cp 1
rs 6.96
c 6
b 0
f 0
wmc 53

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getDockerBinaryRepository() 0 6 1
A captureStepArtifacts() 0 26 4
A imageLogin() 0 4 1
A obtainDockerSocketMount() 0 34 6
A shutdownStepContainer() 0 16 1
A obtainDockerClientMount() 0 19 3
A pipHostConfigBind() 0 8 2
A deployCopy() 0 38 4
B runStep() 0 55 7
A runNewContainer() 0 44 4
B obtainWorkingDirMount() 0 23 9
A captureArtifactPattern() 0 28 4
A __construct() 0 15 1
A obtainServicesNetwork() 0 28 2
A shutdownServices() 0 14 2
A serviceName() 0 8 2

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\Definitions\Service;
12
use Ktomk\Pipelines\File\Image;
13
use Ktomk\Pipelines\File\Pipeline\Step;
14
use Ktomk\Pipelines\Lib;
15
use Ktomk\Pipelines\LibFs;
16
use Ktomk\Pipelines\LibTmp;
17
use Ktomk\Pipelines\Runner\Docker\ArtifactSource;
18
use Ktomk\Pipelines\Runner\Docker\Binary\Repository;
19
use Ktomk\Pipelines\Runner\Docker\ImageLogin;
20
21
/**
22
 * Runner for a single step of a pipeline
23
 */
24
class StepRunner
25
{
26
    /**
27
     * @var RunOpts
28
     */
29
    private $runOpts;
30
31
    /**
32
     * @var Directories
33
     */
34
    private $directories;
35
36
    /**
37
     * @var Exec
38
     */
39
    private $exec;
40
41
    /**
42
     * @var Flags
43
     */
44
    private $flags;
45
46
    /**
47
     * @var Env
48
     */
49
    private $env;
50
51
    /**
52
     * @var Streams
53
     */
54
    private $streams;
55
56
    /**
57
     * list of temporary directory destructible markers
58
     *
59
     * @var array
60
     */
61
    private $temporaryDirectories = array();
62
63
    /**
64
     * DockerSession constructor.
65
     *
66
     * @param RunOpts $runOpts
67
     * @param Directories $directories source repository root directory based directories object
68
     * @param Exec $exec
69
     * @param Flags $flags
70
     * @param Env $env
71
     * @param Streams $streams
72
     */
73 26
    public function __construct(
74
        RunOpts $runOpts,
75
        Directories $directories,
76
        Exec $exec,
77
        Flags $flags,
78
        Env $env,
79
        Streams $streams
80
    )
81
    {
82 26
        $this->runOpts = $runOpts;
83 26
        $this->directories = $directories;
84 26
        $this->exec = $exec;
85 26
        $this->flags = $flags;
86 26
        $this->env = $env;
87 26
        $this->streams = $streams;
88 26
    }
89
90
    /**
91
     * @param Step $step
92
     *
93
     * @return null|int exist status of step script or null if the run operation failed
94
     */
95 25
    public function runStep(Step $step)
96
    {
97 25
        $dir = $this->directories->getProjectDirectory();
98 25
        $env = $this->env;
99 25
        $exec = $this->exec;
100 25
        $streams = $this->streams;
101
102 25
        $env->setPipelinesProjectPath($dir);
103
104 25
        $container = StepContainer::create($step, $exec);
105
106 25
        $name = $container->generateName(
107 25
            $this->runOpts->getPrefix(),
108 25
            $this->env->getValue('BITBUCKET_REPO_SLUG') ?: $this->directories->getName()
109
        );
110 25
        $env->setContainerName($name);
111
112 25
        $image = $step->getImage();
113
114
        # launch container
115 25
        $streams->out(sprintf(
116 25
            "\x1D+++ step #%d\n\n    name...........: %s\n    effective-image: %s\n    container......: %s\n",
117 25
            $step->getIndex() + 1,
118 25
            $step->getName() ? '"' . $step->getName() . '"' : '(unnamed)',
119 25
            $image->getName(),
120
            $name
121
        ));
122
123 25
        $id = $container->keepOrKill($this->flags->reuseContainer());
124
125 25
        $deployCopy = $this->flags->deployCopy();
126
127 25
        if (null === $id) {
128 23
            list($id, $status) = $this->runNewContainer($container, $dir, $deployCopy, $step);
129 22
            if (null === $id) {
130 3
                return $status;
131
            }
132
        }
133
134 21
        $streams->out(sprintf("    container-id...: %s\n\n", substr($id, 0, 12)));
135
136
        # TODO: different deployments, mount (default), mount-ro, copy
137 21
        if (null !== $result = $this->deployCopy($deployCopy, $id, $dir)) {
138 2
            return $result;
139
        }
140
141 19
        $status = StepScriptRunner::createRunStepScript($step, $streams, $exec, $name);
142
143 19
        $this->captureStepArtifacts($step, $deployCopy && 0 === $status, $id, $dir);
144
145 19
        $this->shutdownStepContainer($container, $status);
146
147 19
        $this->shutdownServices($step, $status);
148
149 19
        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
     *
171
     * @throws \RuntimeException
172
     *
173
     * @return void
174
     */
175 19
    private function captureStepArtifacts(Step $step, $copy, $id, $dir)
176
    {
177
        # capturing artifacts is only supported for deploy copy
178 19
        if (!$copy) {
179 14
            return;
180
        }
181
182 5
        $artifacts = $step->getArtifacts();
183
184 5
        if (null === $artifacts) {
185 2
            return;
186
        }
187
188 3
        $exec = $this->exec;
189 3
        $streams = $this->streams;
190
191 3
        $streams->out("\x1D+++ copying artifacts from container...\n");
192
193 3
        $source = new ArtifactSource($exec, $id, $dir);
194
195 3
        $patterns = $artifacts->getPatterns();
196 3
        foreach ($patterns as $pattern) {
197 3
            $this->captureArtifactPattern($source, $pattern, $dir);
198
        }
199
200 3
        $streams('');
201 3
    }
202
203
    /**
204
     * capture artifact pattern
205
     *
206
     * @param ArtifactSource $source
207
     * @param string $pattern
208
     * @param string $dir
209
     *
210
     * @throws \RuntimeException
211
     *
212
     * @return void
213
     *
214
     * @see Runner::captureStepArtifacts()
215
     *
216
     */
217 3
    private function captureArtifactPattern(ArtifactSource $source, $pattern, $dir)
218
    {
219 3
        $exec = $this->exec;
220 3
        $streams = $this->streams;
221
222 3
        $id = $source->getId();
223 3
        $paths = $source->findByPattern($pattern);
224 3
        if (empty($paths)) {
225 1
            return;
226
        }
227
228 2
        $chunks = Lib::arrayChunkByStringLength($paths, 131072, 4);
229
230 2
        foreach ($chunks as $paths) {
231 2
            $docker = Lib::cmd('docker', array('exec', '-w', '/app', $id));
232 2
            $tar = Lib::cmd('tar', array('c', '-f', '-', $paths));
233 2
            $unTar = Lib::cmd('tar', array('x', '-f', '-', '-C', $dir));
234
235 2
            $command = $docker . ' ' . $tar . ' | ' . $unTar;
236 2
            $status = $exec->pass($command, array());
237
238 2
            if (0 !== $status) {
239 1
                $streams->err(sprintf(
240 1
                    "pipelines: Artifact failure: '%s' (%d, %d paths, %d bytes)\n",
241
                    $pattern,
242
                    $status,
243 1
                    count($paths),
244 1
                    strlen($command)
245
                ));
246
            }
247
        }
248 2
    }
249
250
    /**
251
     * @param bool $copy
252
     * @param string $id container id
253
     * @param string $dir directory to copy contents into container
254
     *
255
     * @throws \RuntimeException
256
     *
257
     * @return null|int null if all clear, integer for exit status
258
     */
259 21
    private function deployCopy($copy, $id, $dir)
260
    {
261 21
        if (!$copy) {
262 14
            return null;
263
        }
264
265 7
        $streams = $this->streams;
266 7
        $exec = $this->exec;
267
268 7
        $streams->out("\x1D+++ copying files into container...\n");
269
270 7
        $tmpDir = LibTmp::tmpDir('pipelines-cp.');
271 7
        $this->temporaryDirectories[] = DestructibleString::rmDir($tmpDir);
272 7
        LibFs::symlink($dir, $tmpDir . '/app');
273 7
        $cd = Lib::cmd('cd', array($tmpDir . '/.'));
274 7
        $tar = Lib::cmd('tar', array('c', '-h', '-f', '-', '--no-recursion', 'app'));
275 7
        $dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/.'));
276
        $status = $exec->pass("${cd} && echo 'app' | ${tar} | ${dockerCp}", array());
277
        LibFs::unlink($tmpDir . '/app');
278 7
        if (0 !== $status) {
279 1
            $streams->err('pipelines: deploy copy failure\n');
280
281 1
            return $status;
282
        }
283
284 6
        $cd = Lib::cmd('cd', array($dir . '/.'));
285 6
        $tar = Lib::cmd('tar', array('c', '-f', '-', '.'));
286 6
        $dockerCp = Lib::cmd('docker ', array('cp', '-', $id . ':/app'));
287 6
        $status = $exec->pass("${cd} && ${tar} | ${dockerCp}", array());
288 6
        if (0 !== $status) {
289 1
            $streams->err('pipelines: deploy copy failure\n');
290
291 1
            return $status;
292
        }
293
294 5
        $streams('');
295
296 5
        return null;
297
    }
298
299
    /**
300
     * @param Image $image
301
     *
302
     * @throws \RuntimeException
303
     * @throws \InvalidArgumentException
304
     *
305
     * @return void
306
     */
307
    private function imageLogin(Image $image)
308
    {
309 20
        $login = new ImageLogin($this->exec, $this->env->getResolver());
310 20
        $login->byImage($image);
311 20
    }
312
313
    /**
314
     * @param StepContainer $container
315
     * @param string $dir
316
     * @param bool $copy
317
     * @param Step $step
318
     *
319
     * @return array array(string|null $id, int $status)
320
     */
321
    private function runNewContainer(StepContainer $container, $dir, $copy, Step $step)
322
    {
323 23
        $env = $this->env;
324 23
        $streams = $this->streams;
325
326 23
        $mountDockerSock = $this->obtainDockerSocketMount();
327
328 23
        $mountDockerClient = $this->obtainDockerClientMount($step);
329
330 22
        $mountWorkingDirectory = $this->obtainWorkingDirMount($copy, $dir, $mountDockerSock);
331 22
        if ($mountWorkingDirectory && is_int($mountWorkingDirectory[1])) {
332 2
            return $mountWorkingDirectory;
333
        }
334
335 20
        $network = $this->obtainServicesNetwork($step);
336
337
        # process docker login if image demands so, but continue on failure
338 20
        $image = $step->getImage();
339 20
        $this->imageLogin($image);
340
341 20
        list($status, $out, $err) = $container->run(
342
            array(
343 20
                $network,
344 20
                '-i', '--name', $container->getName(),
345 20
                $env->getArgs('-e'),
346 20
                $env::createArgVarDefinitions('-e', $step->getEnv()),
347 20
                $mountWorkingDirectory, '-e', 'BITBUCKET_CLONE_DIR=/app',
348 20
                $mountDockerSock,
349 20
                $mountDockerClient,
350 20
                '--workdir', '/app', '--detach', '--entrypoint=/bin/sh', $image->getName(),
351
            )
352
        );
353 20
        if (0 !== $status) {
354 1
            $streams->out("    container-id...: *failure*\n\n");
355 1
            $streams->err("pipelines: setting up the container failed\n");
356 1
            $streams->err("${err}\n");
357 1
            $streams->out("${out}\n");
358 1
            $streams->out(sprintf("exit status: %d\n", $status));
359
360 1
            return array(null, $status);
361
        }
362 19
        $id = $container->getDisplayId();
363
364 19
        return array($id, $status);
365
    }
366
367
    /**
368
     * @param Step $step
369
     *
370
     * @return string[]
371
     */
372
    private function obtainDockerClientMount(Step $step)
373
    {
374
        # 'docker.client.path'
375 23
        $path = '/usr/bin/docker';
376
377 23
        if (!$step->getServices()->has('docker')) {
378 20
            return array();
379
        }
380
381
        // prefer pip mount over package
382 3
        $hostPath = $this->pipHostConfigBind($path);
383 3
        if (null !== $hostPath) {
384 1
            return array('-v', sprintf('%s:%s:ro', $hostPath, $path));
385
        }
386
387 2
        $local = $this->getDockerBinaryRepository()->getBinaryPath();
388 1
        chmod($local, 0755);
389
390 1
        return array('-v', sprintf('%s:%s:ro', $local, $path));
391
    }
392
393
    /**
394
     * enable docker client inside docker by mounting docker socket
395
     *
396
     * @return array docker socket volume args for docker run, empty if not mounting
397
     */
398
    private function obtainDockerSocketMount()
399
    {
400 23
        $args = array();
401
402
        // FIXME give more controlling options, this is serious /!\
403 23
        if (!$this->flags->useDockerSocket()) {
404 1
            return $args;
405
        }
406
407 22
        $defaultSocketPath = $this->runOpts->getOption('docker.socket.path');
408 22
        $hostPathDockerSocket = $defaultSocketPath;
409
410
        // pipelines inside pipelines
411 22
        $hostPath = $this->pipHostConfigBind($defaultSocketPath);
412 22
        if (null !== $hostPath) {
413
            return array(
414 1
                '-v', sprintf('%s:%s', $hostPath, $defaultSocketPath),
415
            );
416
        }
417
418 21
        $dockerHost = $this->env->getInheritValue('DOCKER_HOST');
419 21
        if (null !== $dockerHost && 0 === strpos($dockerHost, 'unix://')) {
420 1
            $hostPathDockerSocket = LibFs::normalizePath(substr($dockerHost, 7));
421
        }
422
423 21
        $pathDockerSock = $defaultSocketPath;
424
425 21
        if (file_exists($hostPathDockerSocket)) {
426
            $args = array(
427 18
                '-v', sprintf('%s:%s', $hostPathDockerSocket, $pathDockerSock),
428
            );
429
        }
430
431 21
        return $args;
432
    }
433
434
    /**
435
     * @param bool $copy
436
     * @param string $dir
437
     * @param array $mountDockerSock
438
     *
439
     * @return array mount options or array(null, int $status) for error handling
440
     */
441
    private function obtainWorkingDirMount($copy, $dir, array $mountDockerSock)
442
    {
443 22
        if ($copy) {
444 7
            return array();
445
        }
446
447 15
        $parentName = $this->env->getValue('PIPELINES_PARENT_CONTAINER_NAME');
448 15
        $hostDeviceDir = $this->pipHostConfigBind($dir);
449 15
        $checkMount = $mountDockerSock && null !== $parentName;
450 15
        $deviceDir = $hostDeviceDir ?: $dir;
451 15
        if ($checkMount && '/app' === $dir && null === $hostDeviceDir) { // FIXME(tk): hard encoded /app
452 2
            $deviceDir = $this->env->getValue('PIPELINES_PROJECT_PATH');
453 2
            if ($deviceDir === $dir || null === $deviceDir) {
454 2
                $this->streams->err("pipelines: fatal: can not detect ${dir} mount point. preventing new container.\n");
455
456 2
                return array(null, 1);
457
            }
458
        }
459
460
        // FIXME(tk): Never mount anything not matching /home/[a-zA-Z][a-zA-Z0-9]*/[^.].*/...
461
        //   + do realpath checking
462
        //   + prevent dot path injections (logical fix first)
463 13
        return array('-v', "${deviceDir}:/app"); // FIXME(tk): hard encoded /app
464
    }
465
466
    /**
467
     * get host path from mount point if in pip level 2+
468
     *
469
     * @param mixed $mountPoint
470
     *
471
     * @return null|string
472
     */
473
    private function pipHostConfigBind($mountPoint)
474
    {
475
        // if there is a parent name, this is level 2+
476 23
        if (null === $pipName = $this->env->getValue('PIPELINES_PIP_CONTAINER_NAME')) {
477 20
            return null;
478
        }
479
480 3
        return Docker::create($this->exec)->hostConfigBind($pipName, $mountPoint);
481
    }
482
483
    /**
484
     * @param StepContainer $container
485
     * @param int $status
486
     *
487
     * @return void
488
     */
489
    private function shutdownStepContainer(StepContainer $container, $status)
490
    {
491 19
        $id = $container->getDisplayId();
492
493 19
        $message = sprintf(
494 19
            'keeping container id %s',
495 19
            (string)substr($id, 0, 12)
496
        );
497
498 19
        StepContainer::execShutdownContainer(
499 19
            $this->exec,
500 19
            $this->streams,
501 19
            $id,
502 19
            $status,
503 19
            $this->flags,
504 19
            $message
505
        );
506 19
    }
507
508
    /**
509
     * @param Step $step
510
     *
511
     * @return array docker run options for network (needed if there are services)
512
     *
513
     * @see StepRunner::runNewContainer()
514
     */
515
    private function obtainServicesNetwork(Step $step)
516
    {
517 20
        $services = (array)$step->getServices()->getDefinitions();
518
519 20
        $network = array();
520
521 20
        foreach ($services as $name => $service) {
522 1
            $network = array('--network', 'host');
523 1
            $image = $service->getImage();
524 1
            $this->imageLogin($image);
525
526 1
            $containerName = $this->serviceName($service);
527
528 1
            $resolver = $this->env->getResolver();
529 1
            $variables = $resolver($service->getVariables());
530
531
            $args = array(
532 1
                $network, '--name',
533 1
                $containerName,
534 1
                '--detach',
535 1
                Env::createArgVarDefinitions('-e', $variables),
536 1
                $image->getName(),
537
            );
538
539 1
            $this->exec->capture('docker', Lib::merge('run', $args), $out, $err);
540
        }
541
542 20
        return $network;
543
    }
544
545
    /**
546
     * @param Step $step
547
     * @param int $status
548
     *
549
     * @return void
550
     *
551
     * @see StepRunner::obtainServicesNetwork
552
     * @see StepRunner::shutdownStepContainer
553
     */
554
    private function shutdownServices(Step $step, $status)
555
    {
556 19
        $services = (array)$step->getServices()->getDefinitions();
557
558 19
        foreach ($services as $name => $service) {
559 1
            $name = $this->serviceName($service);
560
561 1
            StepContainer::execShutdownContainer(
562 1
                $this->exec,
563 1
                $this->streams,
564 1
                "/${name}",
565 1
                $status,
566 1
                $this->flags,
567 1
                sprintf('keeping service container %s', $name)
568
            );
569
        }
570 19
    }
571
572
    /**
573
     * @param Service $service
574
     *
575
     * @return string
576
     */
577
    private function serviceName(Service $service)
578
    {
579 1
        $nameSlug = preg_replace(array('( )', '([^a-zA-Z0-9_.-]+)'), array('-', ''), $service->getName());
580 1
        $project = $this->env->getValue('BITBUCKET_REPO_SLUG') ?: $this->directories->getName();
581 1
        $prefix = $this->runOpts->getPrefix();
582 1
        $containerName = $prefix . '-service-' . implode('.', array($nameSlug, $project));
583
584 1
        return $containerName;
585
    }
586
}
587