Passed
Push — test ( ce9d4d...85a148 )
by Tom
02:45
created

CacheIo   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 225
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 83
c 1
b 0
f 1
dl 0
loc 225
ccs 83
cts 83
cp 1
rs 10
wmc 22

7 Methods

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