Passed
Branch test (80d158)
by Tom
02:51
created

CacheIo::deployStepCaches()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 15
nc 4
nop 1
dl 0
loc 27
ccs 16
cts 16
cp 1
crap 5
rs 9.4555
c 1
b 0
f 0
1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines\Runner\Docker;
6
7
use Ktomk\Pipelines\Cli\Exec;
8
use Ktomk\Pipelines\Cli\Streams;
9
use Ktomk\Pipelines\File\Pipeline\Step;
10
use Ktomk\Pipelines\LibFs;
11
use Ktomk\Pipelines\LibFsPath;
12
use Ktomk\Pipelines\Runner\Runner;
13
14
/**
15
 * Class CacheHandler
16
 *
17
 * Directory path related cache operations for docker containers when
18
 * running pipelines.
19
 *
20
 * Delegates between local cache storage (directory based, one tar
21
 * archive per cache) and (running) step-runner docker container.
22
 *
23
 * @package Ktomk\Pipelines\Runner\Docker
24
 */
25
class CacheIo
26
{
27
    /**
28
     * @var string
29
     */
30
    private $id;
31
32
    /**
33
     * @var string
34
     */
35
    private $cachesDirectory;
36
37
    /**
38
     * @var Streams
39
     */
40
    private $streams;
41
42
    /**
43
     * @var Exec
44
     */
45
    private $exec;
46
47
    /**
48
     * @var string
49
     */
50
    private $clonePath;
51
52
    /**
53
     * @var bool
54
     */
55
    private $noCache = false;
56
57
    /**
58
     * @var AbstractionLayer
59
     */
60
    private $dal;
61
62
    /**
63
     * @param Runner $runner
64
     * @param $id
65
     *
66
     * @return CacheIo
67
     */
68 3
    public static function createByRunner(Runner $runner, $id)
69
    {
70 3
        return new self(
71 3
            self::runnerCachesDirectory($runner),
72
            $id,
73 3
            $runner->getRunOpts()->getOption('step.clone-path'),
74 3
            $runner->getStreams(),
75 3
            $runner->getExec(),
76 3
            $runner->getFlags()->noCache()
77
        );
78
    }
79
80
    /**
81
     * @param Runner $runner
82
     *
83
     * @return string path of base-directory in which the pipelines project tar files are located
84
     */
85 4
    public static function runnerCachesDirectory(Runner $runner)
86
    {
87 4
        return (string)$runner->getDirectories()->getBaseDirectory(
88 4
            'XDG_CACHE_HOME',
89 4
            sprintf(
90 4
                'caches/%s',
91 4
                $runner->getProject()
92
            )
93
        );
94
    }
95
96
    /**
97
     * CacheIo constructor.
98
     *
99
     * @param string $caches path to directory where tar files are stored
100
     * @param string $id container
101
     * @param string $clonePath
102
     * @param Streams $streams
103
     * @param Exec $exec
104
     * @param bool $noCache
105
     */
106 10
    public function __construct($caches, $id, $clonePath, Streams $streams, Exec $exec, $noCache)
107
    {
108 10
        $this->id = $id;
109 10
        $this->setCachesDirectory($caches);
110 9
        $this->clonePath = $clonePath;
111 9
        $this->streams = $streams;
112 9
        $this->exec = $exec;
113 9
        $this->dal = new AbstractionLayerImpl($exec);
114 9
        $this->noCache = $noCache;
115 9
    }
116
117
    /**
118
     * Deploy step caches into container
119
     *
120
     * @param Step $step
121
     */
122 5
    public function deployStepCaches(Step $step)
123
    {
124 5
        $cachesDirectory = $this->cachesDirectory;
125
126
        // skip deploying caches if there are no caches
127 5
        if ($this->skip($this->noCache, $cachesDirectory, $step)) {
128 3
            return;
129
        }
130
131 2
        $id = $this->id;
132 2
        $streams = $this->streams;
133
134 2
        $streams->out("\x1D+++ populating caches...\n");
135
136 2
        foreach ($step->getCaches() as $name => $path) {
137 2
            $tarFile = sprintf('%s/%s.tar', $cachesDirectory, $name);
138 2
            $tarExists = LibFs::isReadableFile($tarFile);
139 2
            $streams->out(sprintf(" - %s %s (%s)\n", $name, $path, $tarExists ? 'hit' : 'miss'));
140
141 2
            if (!$tarExists) {
142 1
                continue;
143
            }
144
145 1
            $containerPath = $this->mapCachePath($path);
146
147 1
            $this->dal->execute($id, array('mkdir', '-p', $containerPath));
148 1
            $this->dal->importTar($tarFile, $id, $containerPath);
149
        }
150 2
    }
151
152
    /**
153
     * Capture cache from container
154
     *
155
     * @param Step $step
156
     * @param bool $capture
157
     */
158 6
    public function captureStepCaches(Step $step, $capture)
159
    {
160
        // skip capturing if disabled
161 6
        if (!$capture) {
162 1
            return;
163
        }
164
165 5
        $cachesDirectory = $this->cachesDirectory;
166
167
        // skip capturing caches if there are no caches
168 5
        if ($this->skip($this->noCache, $cachesDirectory, $step)) {
169 3
            return;
170
        }
171
172 2
        $id = $this->id;
173 2
        $exec = $this->exec;
0 ignored issues
show
Unused Code introduced by
The assignment to $exec is dead and can be removed.
Loading history...
174 2
        $streams = $this->streams;
175
176 2
        $streams->out("\x1D+++ updating caches from container...\n");
177
178 2
        $cachesDirectory = LibFs::mkDir($cachesDirectory);
179
180 2
        foreach ($step->getCaches() as $name => $path) {
181 2
            $tarFile = sprintf('%s/%s.tar', $cachesDirectory, $name);
182 2
            $tarExists = LibFs::isReadableFile($tarFile);
183 2
            $streams->out(sprintf(" - %s %s (%s)\n", $name, $path, $tarExists ? 'update' : 'create'));
184
185 2
            $containerPath = $this->mapCachePath($path);
186 2
            $this->dal->exportTar($id, $containerPath . '/.', $tarFile);
187
        }
188 2
    }
189
190
    /**
191
     * Map cache path to container path
192
     *
193
     * A cache path in a cache definition can be with information for
194
     * container context, e.g. $HOME, ~ or just being relative to the
195
     * clone directory.
196
     *
197
     * This requires mapping for the "real" path
198
     *
199
     * @param string $path component of cache definition
200
     *
201
     * @return string
202
     */
203 4
    public function mapCachePath($path)
204
    {
205 4
        $id = $this->id;
206
207 4
        $containerPath = $path;
208
209
        // let the container map the path expression
210 4
        $out = $this->dal->execute($id, array('/bin/sh', '-c', sprintf('echo %s', $containerPath)));
211 4
        if (0 < strlen((string)$out) && ('/' === $out[0])) {
212 1
            $containerPath = trim($out);
213
        }
214
215
        // fallback to '~' -> /root assumption (can fail easy as root might not be the user)
216 4
        if ('~' === $containerPath[0]) {
217 2
            $containerPath = '/root/' . ltrim(substr($containerPath, 1), '/');
218
        }
219
220
        // handle relative paths
221 4
        if (!LibFsPath::isAbsolute($containerPath)) {
222 1
            $containerPath = LibFsPath::normalizeSegments(
223 1
                $this->clonePath . '/' . $containerPath
224
            );
225
        }
226
227 4
        return $containerPath;
228
    }
229
230
    /**
231
     * @param bool $noCache
232
     * @param string $cachesDirectory
233
     * @param Step $step
234
     *
235
     * @return bool|void
236
     */
237 5
    private function skip($noCache, $cachesDirectory, Step $step)
238
    {
239 5
        if ($noCache) {
240 1
            return true;
241
        }
242
243
        // caches directory is empty?
244 4
        if (empty($cachesDirectory)) {
245 1
            return true;
246
        }
247
248
        // step has caches?
249 3
        $caches = $step->getCaches()->getIterator();
250 3
        if (!count($caches)) {
251 1
            return true;
252
        }
253
254 2
        return false;
255
    }
256
257
    /**
258
     * @param string $path
259
     */
260 10
    private function setCachesDirectory($path)
261
    {
262 10
        if (!empty($path) && !LibFsPath::isAbsolute($path)) {
263 1
            throw new \InvalidArgumentException(sprintf('Caches directory: Not an absolute path: %s', $path));
264
        }
265
266 9
        $this->cachesDirectory = (string)$path;
267 9
    }
268
}
269