Passed
Push — test ( f5878f...bcdd58 )
by Tom
07:12
created

StepRunner::runStep()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 61
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 35
c 2
b 0
f 0
nc 10
nop 1
dl 0
loc 61
ccs 36
cts 36
cp 1
crap 9
rs 8.0555

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