Passed
Push — master ( 94154d...6015fa )
by Tom
02:39
created

Runner::runStep()   C

Complexity

Conditions 11
Paths 48

Size

Total Lines 77
Code Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 11.0022

Importance

Changes 0
Metric Value
cc 11
eloc 50
nc 48
nop 2
dl 0
loc 77
ccs 37
cts 38
cp 0.9737
crap 11.0022
rs 5.3974
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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;
6
7
use Ktomk\Pipelines\Cli\Docker;
8
use Ktomk\Pipelines\Cli\Exec;
9
use Ktomk\Pipelines\Cli\Streams;
10
use Ktomk\Pipelines\File\Image;
11
use Ktomk\Pipelines\Runner\ArtifactSource;
12
use Ktomk\Pipelines\Runner\DockerLogin;
13
use Ktomk\Pipelines\Runner\Env;
14
15
/**
16
 * Pipeline runner with docker under the hood
17
 */
18
class Runner
19
{
20
    const FLAGS = 19;
21
    const FLAG_DOCKER_REMOVE = 1;
22
    const FLAG_DOCKER_KILL = 2;
23
    const FLAG_DEPLOY_COPY = 4; # copy working dir into container
24
    const FLAG_KEEP_ON_ERROR = 8;
25
    const FLAG_SOCKET = 16;
26
27
    const STATUS_NO_STEPS = 1;
28
    const STATUS_RECURSION_DETECTED = 127;
29
30
    /**
31
     * @var string
32
     */
33
    private $prefix;
34
35
    /**
36
     * @var string
37
     */
38
    private $directory;
39
40
    /**
41
     * @var Exec
42
     */
43
    private $exec;
44
45
    /**
46
     * @var int
47
     */
48
    private $flags;
49
50
    /**
51
     * @var Env
52
     */
53
    private $env;
54
    /**
55
     * @var Streams
56
     */
57
    private $streams;
58
59
    /**
60
     * DockerSession constructor.
61
     *
62
     * @param string $prefix
63
     * @param string $directory source repository root
64
     * @param Exec $exec
65
     * @param int $flags [optional]
66
     * @param Env $env [optional]
67
     * @param Streams $streams [optional]
68
     */
69 12
    public function __construct(
70
        $prefix,
71
        $directory,
72
        Exec $exec,
73
        $flags = null,
74
        Env $env = null,
75
        Streams $streams = null
76
    )
77
    {
78 12
        $this->prefix = $prefix;
79
80 12
        $this->directory = $directory;
81 12
        $this->exec = $exec;
82 12
        $this->flags = null === $flags ? self::FLAGS : $flags;
83 12
        $this->env = null === $env ? Env::create() : $env;
84 12
        $this->streams = null === $streams ? Streams::create() : $streams;
85 12
    }
86
87
    /**
88
     * @param Pipeline $pipeline
89
     * @throws \RuntimeException
90
     * @return int
91
     */
92 12
    public function run(Pipeline $pipeline)
93
    {
94 12
        $hasId = $this->env->setPipelinesId($pipeline->getId()); # TODO give Env an addPipeline() method (compare addReference)
95 12
        if ($hasId) {
96 1
            $this->streams->err(sprintf(
97 1
                "pipelines: won't start pipeline '%s'; pipeline inside pipelines recursion detected\n",
98 1
                $pipeline->getId()
99
            ));
100
101 1
            return self::STATUS_RECURSION_DETECTED;
102
        }
103
104 11
        $steps = $pipeline->getSteps();
105 11
        foreach ($steps as $index => $step) {
106 10
            $status = $this->runStep($step, $index);
107 10
            if (0 !== $status) {
108 10
                return $status;
109
            }
110
        }
111
112 7
        if (!isset($status)) {
113 1
            $this->streams->err("pipelines: pipeline with no step to execute\n");
114
115 1
            return self::STATUS_NO_STEPS;
116
        }
117
118 6
        return $status;
119
    }
120
121
    /**
122
     * @param Step $step
123
     * @param int $index
124
     * @throws \RuntimeException
125
     * @return int exit status
126
     */
127 10
    public function runStep(Step $step, $index)
128
    {
129 10
        $prefix = $this->prefix;
130 10
        $dir = $this->directory;
131 10
        $env = $this->env;
132 10
        $exec = $this->exec;
133 10
        $streams = $this->streams;
134
135 10
        $docker = new Docker($exec);
136
137 10
        $name = $prefix . '-' . Lib::generateUuid();
138 10
        $image = $step->getImage();
139 10
        $env->setContainerName($name);
140
141
        # launch container
142 10
        $streams->out(sprintf(
143 10
            "\x1D+++ step #%d\n\n    name...........: %s\n    effective-image: %s\n    container......: %s\n",
144 10
            $index,
145 10
            $step->getName() ? '"' . $step->getName() . '"' : '(unnamed)',
146 10
            $image->getName(),
147 10
            $name
148
        ));
149
150
        # process docker login if image demands so, but continue on failure
151 10
        $this->imageLogin($image);
152
153 10
        $copy = (bool)($this->flags & self::FLAG_DEPLOY_COPY);
154
155
        // enable docker client inside docker by mounting docker socket
156
        // FIXME give controlling options, this is serious /!\
157 10
        $socket = (bool)($this->flags & self::FLAG_SOCKET);
158 10
        $mountDockerSock = array();
159 10
        if ($socket && file_exists('/var/run/docker.sock')) {
160
            $mountDockerSock = array(
161
                '-v', '/var/run/docker.sock:/var/run/docker.sock',
162
            );
163
        }
164
165 10
        $parentName = $env->getValue('PIPELINES_PARENT_CONTAINER_NAME');
166 10
        $checkMount = $mountDockerSock && null !== $parentName;
167 10
        $deviceDir = $checkMount ? $docker->hostDevice($parentName, $dir) : $dir;
168
169 10
        $mountWorkingDirectory = $copy
170 6
            ? array()
171 10
            : array('--volume', "${deviceDir}:/app"); // FIXME(tk): hard encoded /app
172
173
        $status = $exec->capture('docker', array(
174
            'run', '-i', '--name', $name,
175
            $env->getArgs('-e'),
176
            $mountWorkingDirectory, '-e', 'BITBUCKET_CLONE_DIR=/app',
177
            $mountDockerSock,
178
            '--workdir', '/app', '--detach', $image->getName()
179
        ), $out, $err);
180
        if (0 !== $status) {
181
            $streams->out("    container-id...: *failure*\n\n");
182
            $streams->err("pipelines: setting up the container failed\n");
183
            $streams->err("${err}\n");
184
            $streams->out("${out}\n");
185 1
            $streams->out(sprintf("exit status: %d\n", $status));
186
187 1
            return $status;
188
        }
189 9
        $id = rtrim($out) ?: "*dry-run*"; # side-effect: internal exploit of no output with true exit status
190 9
        $streams->out(sprintf("    container-id...: %s\n\n", substr($id, 0, 12)));
191
192
        # TODO: different deployments, mount (default), mount-ro, copy
193 9
        if (null !== $result = $this->deployCopy($copy, $id, $dir)) {
194 2
            return $result;
195
        }
196
197 7
        $status = $this->runStepScript($step, $streams, $exec, $name);
198
199 7
        $this->captureStepArtifacts($step, $copy && 0 === $status, $id, $dir);
200
201 7
        $this->shutdownStepContainer($status, $id, $exec, $name);
202
203 7
        return $status;
204
    }
205
206
    /**
207
     * @param Image $image
208
     * @throws \RuntimeException
209
     * @throws \InvalidArgumentException
210
     */
211
    private function imageLogin(Image $image)
212
    {
213 10
        $login = new DockerLogin($this->exec, $this->env->getResolver());
214 10
        $login->byImage($image);
215 10
    }
216
217
    /**
218
     * @param bool $copy
219
     * @param string $id container id
220
     * @param string $dir directory to copy contents into container
221
     * @throws \RuntimeException
222
     * @return null|int null if all clear, integer for exit status
223
     */
224
    private function deployCopy($copy, $id, $dir)
225
    {
226 9
        if (!$copy) {
227 3
            return null;
228
        }
229
230 6
        $streams = $this->streams;
231 6
        $exec = $this->exec;
232
233 6
        $streams->out("\x1D+++ copying files into container...\n");
234
235 6
        $tmpDir = sys_get_temp_dir() . '/pipelines/cp';
236 6
        Lib::fsMkdir($tmpDir);
237 6
        Lib::fsSymlink($dir, $tmpDir . '/app');
238 6
        $cd = Lib::cmd('cd', array($tmpDir . '/.'));
239 6
        $tar = Lib::cmd(
240 6
            'tar',
241
            array(
242 6
                'c', '-h', '-f', '-', '--no-recursion', 'app')
243
        );
244 6
        $dockerCp = Lib::cmd(
245 6
            'docker ',
246
            array(
247 6
                'cp', '-', $id . ':/.')
248
        );
249 6
        $status = $exec->pass("${cd} && echo 'app' | ${tar} | ${dockerCp}", array());
250 6
        Lib::fsUnlink($tmpDir . '/app');
251 6
        if (0 !== $status) {
252 1
            $streams->err('pipelines: deploy copy failure\n');
253
254 1
            return $status;
255
        }
256
257 5
        $cd = Lib::cmd('cd', array($dir . '/.'));
258 5
        $tar = Lib::cmd(
259 5
            'tar',
260
            array(
261 5
                'c', '-f', '-', '.')
262
        );
263 5
        $dockerCp = Lib::cmd(
264 5
            'docker ',
265
            array(
266 5
                'cp', '-', $id . ':/app')
267
        );
268 5
        $status = $exec->pass("${cd} && ${tar} | ${dockerCp}", array());
269 5
        if (0 !== $status) {
270 1
            $streams->err('pipelines: deploy copy failure\n');
271
272 1
            return $status;
273
        }
274
275 4
        $streams("");
276
277 4
        return null;
278
    }
279
280
    /**
281
     * @param Step $step
282
     * @param Streams $streams
283
     * @param Exec $exec
284
     * @param string $name container name
285
     * @return null|int should never be null, status, non-zero if a command failed
286
     */
287
    private function runStepScript(Step $step, Streams $streams, Exec $exec, $name)
288
    {
289 7
        $script = $step->getScript();
290 7
        $status = null;
291
292 7
        foreach ($script as $line => $command) {
293 7
            $streams->out(sprintf("\x1D+ %s\n", $command));
294 7
            $status = $exec->pass('docker', array(
295 7
                'exec', '-i', $name, '/bin/sh', '-c', $command,
296
            ));
297 7
            $streams->out(sprintf("\n"));
298 7
            if (0 !== $status) {
299 7
                break;
300
            }
301
        }
302
303 7
        return $status;
304
    }
305
306
    /**
307
     * @param Step $step
308
     * @param bool $copy
309
     * @param string $id container id
310
     * @param string $dir to put artifacts in (project directory)
311
     * @throws \RuntimeException
312
     */
313
    private function captureStepArtifacts(Step $step, $copy, $id, $dir)
314
    {
315
        # capturing artifacts is only supported for deploy copy
316 7
        if (!$copy) {
317 3
            return;
318
        }
319
320 4
        $artifacts = $step->getArtifacts();
321
322 4
        if (null === $artifacts) {
323 1
            return;
324
        }
325
326 3
        $exec = $this->exec;
327 3
        $streams = $this->streams;
328
329 3
        $streams->out("\x1D+++ copying artifacts from container...\n");
330
331 3
        $source = new ArtifactSource($exec, $id, $dir);
332
333 3
        $patterns = $artifacts->getPatterns();
334 3
        foreach ($patterns as $pattern) {
335 3
            $this->captureArtifactPattern($source, $pattern, $dir);
336
        }
337
338 3
        $streams("");
339 3
    }
340
341
    /**
342
     * @see Runner::captureStepArtifacts()
343
     *
344
     * @param ArtifactSource $source
345
     * @param string $pattern
346
     * @param string $dir
347
     * @throws \RuntimeException
348
     */
349
    private function captureArtifactPattern(ArtifactSource $source, $pattern, $dir)
350
    {
351 3
        $exec = $this->exec;
352 3
        $streams = $this->streams;
353
354 3
        $id = $source->getId();
355 3
        $paths = $source->findByPattern($pattern);
356 3
        if (empty($paths)) {
357 1
            return;
358
        }
359
360 2
        $chunkSize = 2048;
361 2
        $chunks = array_chunk($paths, $chunkSize, true);
362
363 2
        foreach ($chunks as $paths) {
364 2
            $docker = Lib::cmd('docker', array('exec', '-w', '/app', $id));
365 2
            $tar = Lib::cmd('tar', array('c', '-f', '-', $paths));
366 2
            $unTar = Lib::cmd('tar', array('x', '-f', '-', '-C', $dir));
367
368 2
            $status = $exec->pass($docker . ' ' . $tar . ' | ' . $unTar, array());
369
370 2
            if (0 !== $status) {
371 1
                $streams->err(
372 2
                    sprintf("pipelines: Artifact failure: '%s' (%d, %d paths)\n", $pattern, $status, count($paths))
373
                );
374
            }
375
        }
376 2
    }
377
378
    /**
379
     * @param int $status
380
     * @param string $id container id
381
     * @param Exec $exec
382
     * @param string $name container name
383
     * @throws \RuntimeException
384
     */
385
    private function shutdownStepContainer($status, $id, Exec $exec, $name)
386
    {
387 7
        $flags = $this->flags;
388
389
        # keep container on error
390 7
        if (0 !== $status && $flags & self::FLAG_KEEP_ON_ERROR) {
391 1
            $this->streams->err(sprintf(
392 1
                "exit status %d, keeping container id %s\n",
393 1
                $status,
394 1
                substr($id, 0, 12)
395
            ));
396
397 1
            return;
398
        }
399
400
        # keep or remove container
401 6
        if ($flags & self::FLAG_DOCKER_KILL) {
402 2
            $exec->capture('docker', array('kill', $name));
403
        }
404
405 6
        if ($flags & self::FLAG_DOCKER_REMOVE) {
406 2
            $exec->capture('docker', array('rm', $name));
407
        }
408 6
    }
409
}
410