StepRunner   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
eloc 156
c 8
b 0
f 1
dl 0
loc 385
ccs 156
cts 156
cp 1
rs 8.5599
wmc 48

11 Methods

Rating   Name   Duplication   Size   Complexity  
A obtainDockerSocketMount() 0 34 6
A captureStepArtifacts() 0 28 5
A pipHostConfigBind() 0 8 2
A deployCopy() 0 19 3
A getDockerBinaryRepository() 0 6 1
B obtainWorkingDirMount() 0 24 9
A __construct() 0 3 1
A obtainDockerClientMount() 0 18 4
A runNewContainer() 0 40 4
B runStep() 0 65 9
A captureArtifactPattern() 0 30 4

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