Passed
Push — master ( 0cfc7e...b88fc1 )
by Tom
03:33 queued 42s
created

Runner::runStep()   C

Complexity

Conditions 9
Paths 48

Size

Total Lines 72
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 47
CRAP Score 9.0007

Importance

Changes 0
Metric Value
cc 9
eloc 48
nc 48
nop 2
dl 0
loc 72
ccs 47
cts 48
cp 0.9792
crap 9.0007
rs 6.0413
c 0
b 0
f 0

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