Passed
Push — test ( 0c059e...cfabb2 )
by Tom
02:51
created

StepRunner::obtainServicesNetwork()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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