Passed
Push — master ( 6015fa...83e3bb )
by Tom
02:48
created

App::parseExec()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 0
dl 0
loc 15
ccs 9
cts 9
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/* this file is part of pipelines */
4
5
namespace Ktomk\Pipelines\Utility;
6
7
use Exception;
8
use InvalidArgumentException;
9
use Ktomk\Pipelines\Cli\Args;
10
use Ktomk\Pipelines\Cli\ArgsException;
11
use Ktomk\Pipelines\Cli\Exec;
12
use Ktomk\Pipelines\Cli\Streams;
13
use Ktomk\Pipelines\File;
14
use Ktomk\Pipelines\Lib;
15
use Ktomk\Pipelines\Pipeline;
16
use Ktomk\Pipelines\Runner;
17
use Ktomk\Pipelines\Runner\Env;
18
19
class App implements Runnable
20
{
21
    const BBPL_BASENAME = 'bitbucket-pipelines.yml';
22
23
    const VERSION = '@.@.@';
24
25
    /**
26
     * @var Args
27
     */
28
    private $arguments;
29
30
    /**
31
     * @var Streams
32
     */
33
    private $streams;
34
35
    /**
36
     * @var bool
37
     */
38
    private $verbose = true;
39
40
    /**
41
     * @var Help
42
     */
43
    private $help;
44
45 29
    public function __construct(Streams $streams)
46
    {
47 29
        $this->streams = $streams;
48 29
        $this->help = new Help($streams);
49 29
    }
50
51
    /**
52
     * @return App
53
     */
54 1
    public static function create()
55
    {
56 1
        $streams = Streams::create();
57
58 1
        return new self($streams);
59
    }
60
61
    /**
62
     * @param array $arguments including the utility name in the first argument
63
     * @throws InvalidArgumentException
64
     * @return int 0-255
65
     */
66 28
    public function main(array $arguments)
67
    {
68 28
        $args = Args::create($arguments);
69
70 28
        $this->verbose = $args->hasOption(array('v', 'verbose'));
71 28
        $this->arguments = $args;
72
73 28
        $handler = new ExceptionHandler(
74 28
            $this->streams,
75 28
            $this->help,
76 28
            $args->hasOption('debug')
77
        );
78
79 28
        return $handler->handle($this);
80
    }
81
82
    /**
83
     * @throws \UnexpectedValueException
84
     * @throws \RuntimeException
85
     * @throws InvalidArgumentException
86
     * @throws \Ktomk\Pipelines\File\ParseException
87
     * @throws ArgsException
88
     * @throws StatusException
89
     * @return int
90
     */
91 28
    public function run()
92
    {
93 28
        $args = $this->arguments;
94
95 28
        $this->help->run($args);
96
97 26
        $prefix = $this->parsePrefix();
98
99 23
        $exec = $this->parseExec();
100
101 23
        DockerOptions::bind($args, $exec, $prefix, $this->streams)->run();
102
103 22
        $keep = KeepOptions::bind($args)->run();
104
105 21
        $basename = $this->parseBasename();
106
107 20
        $workingDir = $this->parseWorkingDir();
108
109 19
        $path = $this->parsePath($basename, $workingDir);
110
111
        // TODO: obtain project dir information etc. from VCS
112
        // $vcs = new Vcs();
113
114 16
        $noRun = $args->hasOption('no-run');
115
116 16
        $deployMode = $this->parseDeployMode();
117
118 15
        $pipelines = File::createFromFile($path);
119
120 15
        $fileOptions = FileOptions::bind($args, $this->streams, $pipelines)->run();
121
122 11
        $reference = $this->parseReference();
123
124 11
        $env = $this->parseEnv($_SERVER, $reference, $workingDir);
125
126 11
        $pipelineId = $pipelines->searchIdByReference($reference) ?: 'default';
127
128 11
        $pipelineId = $args->getOptionArgument('pipeline', $pipelineId);
129
130 11
        $streams = $this->parseStreams();
131
132 11
        $this->parseRemainingOptions();
133
134 10
        $pipeline = $this->getRunPipeline($pipelines, $pipelineId, $fileOptions);
135
136 7
        $flags = $this->getRunFlags($keep, $deployMode);
137
138 7
        $runner = new Runner($prefix, $workingDir, $exec, $flags, $env, $streams);
139
140 7
        if ($noRun) {
141 2
            $this->verbose('info: not running the pipeline per --no-run option');
142 2
            $status = 0;
143
        } else {
144 5
            $status = $runner->run($pipeline);
145 5
            if (0 !== $status) {
146
                $this->streams->out(sprintf("exit status: %d\n", $status)); // @codeCoverageIgnore
147
            }
148
        }
149
150 7
        return $status;
151
    }
152
153
    /**
154
     * @throws StatusException
155
     * @return string
156
     */
157 19
    private function getWorkingDirectory()
158
    {
159 19
        $workingDir = \getcwd();
160 19
        if (false === $workingDir) {
161
            // @codeCoverageIgnoreStart
162
            StatusException::status(1, 'fatal: obtain working directory');
163
            // @codeCoverageIgnoreEnd
164
        }
165
166 19
        return $workingDir;
167
    }
168
169
    /**
170
     * @throws InvalidArgumentException
171
     * @throws StatusException
172
     * @throws ArgsException
173
     * @return string basename for bitbucket-pipelines.yml
174
     */
175 21
    private function parseBasename()
176
    {
177 21
        $args = $this->arguments;
178
179 21
        $basename = $args->getOptionArgument('basename', self::BBPL_BASENAME);
180 21
        if (!Lib::fsIsBasename($basename)) {
181 1
            StatusException::status(1, sprintf("not a basename: '%s'", $basename));
182
        }
183
184 20
        return $basename;
185
    }
186
187
    /**
188
     * @throws InvalidArgumentException
189
     * @throws ArgsException
190
     * @throws StatusException
191
     * @return string deploy mode ('copy', 'mount')
192
     */
193 16
    private function parseDeployMode()
194
    {
195 16
        $args = $this->arguments;
196
197 16
        $deployMode = $args->getOptionArgument('deploy', 'copy');
198
199 16
        if (!in_array($deployMode, array('mount', 'copy'), true)) {
200 1
            StatusException::status(
201 1
                1,
202 1
                sprintf("unknown deploy mode '%s'\n", $deployMode)
203
            );
204
        }
205
206 15
        return $deployMode;
207
    }
208
209
    /**
210
     * @param array $inherit from this environment
211
     * @param Runner\Reference $reference
212
     * @param string $workingDir
213
     * @throws InvalidArgumentException
214
     * @throws ArgsException
215
     * @return Env
216
     */
217 11
    private function parseEnv(array $inherit, $reference, $workingDir)
218
    {
219 11
        $args = $this->arguments;
220
221 11
        $env = Env::create($inherit);
222 11
        $env->addReference($reference);
223 11
        $env->collectFiles(array(
224 11
            $workingDir . '/.env.dist',
225 11
            $workingDir . '/.env',
226
        ));
227 11
        $env->collect($args, array('e', 'env', 'env-file'));
228
229 11
        return $env;
230
    }
231
232
    /**
233
     * @throws InvalidArgumentException
234
     * @return Exec
235
     */
236 23
    private function parseExec()
237
    {
238 23
        $args = $this->arguments;
239
240 23
        $debugPrinter = null;
241 23
        if ($this->verbose) {
242 3
            $debugPrinter = $this->streams;
243
        }
244 23
        $exec = new Exec($debugPrinter);
245
246 23
        if ($args->hasOption('dry-run')) {
247 6
            $exec->setActive(false);
248
        }
249
250 23
        return $exec;
251
    }
252
253
    /**
254
     * @param string $basename
255
     * @param string $workingDir
256
     * @throws InvalidArgumentException
257
     * @throws StatusException
258
     * @throws ArgsException
259
     * @return string file
260
     */
261 19
    private function parseFile($basename, &$workingDir)
262
    {
263 19
        $args = $this->arguments;
264
265
        /** @var string $file as bitbucket-pipelines.yml to process */
266 19
        $file = $args->getOptionArgument('file', null);
267 19
        if (null === $file && null !== $file = Lib::fsFileLookUp($basename, $workingDir)) {
0 ignored issues
show
introduced by
The condition null === $file is always false.
Loading history...
268 13
            $buffer = dirname($file);
269 13
            if ($buffer !== $workingDir) {
270 1
                $this->changeWorkingDir($buffer);
271 1
                $workingDir = $this->getWorkingDirectory();
272
            }
273
        }
274
275 19
        if (!strlen($file)) {
276 1
            StatusException::status(1, 'file can not be empty');
277
        }
278
279 18
        return $file;
280
    }
281
282
    /**
283
     * @param string $basename
284
     * @param string $workingDir
285
     * @throws InvalidArgumentException
286
     * @throws ArgsException
287
     * @throws StatusException
288
     * @return string path
289
     */
290 19
    private function parsePath($basename, &$workingDir)
291
    {
292 19
        $file = $this->parseFile($basename, $workingDir);
293
294 18
        if ($file !== $basename && self::BBPL_BASENAME !== $basename) {
295 1
            $this->verbose('info: --file overrides non-default --basename');
296
        }
297
298
        /** @var string $path full path as bitbucket-pipelines.yml to process */
299 18
        $path = Lib::fsIsAbsolutePath($file)
300 14
            ? $file
301 18
            : $workingDir . '/' . $file;
302
303 18
        if (!is_file($path) && !is_readable($path)) {
304 2
            StatusException::status(1, sprintf('not a readable file: %s', $file));
305
        }
306
307 16
        $this->verbose(sprintf("info: pipelines file is '%s'", $path));
308
309 16
        return $path;
310
    }
311
312
    /**
313
     * @throws InvalidArgumentException
314
     * @throws ArgsException
315
     * @return string
316
     */
317 26
    private function parsePrefix()
318
    {
319 26
        $args = $this->arguments;
320
321 26
        $prefix = $args->getOptionArgument('prefix', 'pipelines');
322 24
        if (!preg_match('~^[a-z]{3,}$~', $prefix)) {
323 1
            ArgsException::__(sprintf("invalid prefix: '%s'", $prefix));
324
        }
325
326 23
        return $prefix;
327
    }
328
329
    /**
330
     * @throws InvalidArgumentException
331
     * @throws ArgsException
332
     * @return Runner\Reference
333
     */
334 11
    private function parseReference()
335
    {
336 11
        $trigger = $this->arguments->getOptionArgument('trigger');
337
338 11
        return Runner\Reference::create($trigger);
339
    }
340
341
    /**
342
     * give error about unknown option, show usage and exit status of 1
343
     *
344
     * @throws ArgsException
345
     */
346 11
    private function parseRemainingOptions()
347
    {
348 11
        $option = $this->arguments->getFirstRemainingOption();
349
350 11
        if ($option) {
351 1
            ArgsException::__(
352 1
                sprintf('unknown option: %s', $option)
353
            );
354
        }
355 10
    }
356
357
    /**
358
     * @throws InvalidArgumentException
359
     * @return Streams
360
     */
361 11
    private function parseStreams()
362
    {
363 11
        $streams = $this->streams;
364
365
        // --verbatim show only errors for own runner actions, show everything from pipeline verbatim
366 11
        if ($this->arguments->hasOption('verbatim')) {
367 1
            $streams = new Streams();
368 1
            $streams->copyHandle($this->streams, 2);
369
        }
370
371 11
        return $streams;
372
    }
373
374
    /**
375
     * @throws InvalidArgumentException
376
     * @throws StatusException
377
     * @throws ArgsException
378
     * @return string current working directory
379
     */
380 20
    private function parseWorkingDir()
381
    {
382 20
        $args = $this->arguments;
383
384 20
        $buffer = $args->getOptionArgument('working-dir', false);
385
386 20
        if (false !== $buffer) {
387 2
            $this->changeWorkingDir($buffer);
388
        }
389
390 19
        return $this->getWorkingDirectory();
391
    }
392
393
    /**
394
     * @param string $directory
395
     * @throws StatusException
396
     */
397 2
    private function changeWorkingDir($directory)
398
    {
399 2
        $this->verbose(
400 2
            sprintf('info: changing working directory to %s', $directory)
401
        );
402
403 2
        $result = chdir($directory);
404 2
        if (false === $result) {
405 1
            StatusException::status(
406 1
                2,
407 1
                sprintf('fatal: change working directory to %s', $directory)
408
            );
409
        }
410 1
    }
411
412
    /**
413
     * Obtain pipeline to run from file while handling error output
414
     *
415
     * @param File $pipelines
416
     * @param $pipelineId
417
     * @param FileOptions $fileOptions
418
     * @throws \Ktomk\Pipelines\File\ParseException
419
     * @throws StatusException
420
     * @return Pipeline on success
421
     */
422 10
    private function getRunPipeline(File $pipelines, $pipelineId, FileOptions $fileOptions)
423
    {
424 10
        $this->verbose(sprintf("info: running pipeline '%s'", $pipelineId));
425
426
        try {
427 10
            $pipeline = $pipelines->getById($pipelineId);
428 2
        } catch (File\ParseException $e) {
429 1
            $this->error(sprintf("pipelines: error: pipeline id '%s'", $pipelineId));
430
431 1
            throw $e;
432 1
        } catch (InvalidArgumentException $e) {
433 1
            $this->error(sprintf("pipelines: pipeline '%s' unavailable", $pipelineId));
434 1
            $this->info('Pipelines are:');
435 1
            $fileOptions->showPipelines($pipelines);
436 1
            StatusException::status(1);
437
        }
438
439 8
        if (!$pipeline) {
440 1
            StatusException::status(1, 'no pipeline to run!');
441
        }
442
443 7
        return $pipeline;
444
    }
445
446 2
    private function error($message)
447
    {
448 2
        $this->streams->err(
449 2
            sprintf("%s\n", $message)
450
        );
451 2
    }
452
453 4
    private function info($message)
454
    {
455 4
        $this->streams->out(
456 4
            sprintf("%s\n", $message)
457
        );
458 4
    }
459
460 18
    private function verbose($message)
461
    {
462 18
        if ($this->verbose) {
463 3
            $this->info($message);
464
        }
465 18
    }
466
467
    /**
468
     * Map diverse parameters to run flags
469
     *
470
     * @param KeepOptions $keep
471
     * @param $deployMode
472
     * @return bool|int
473
     */
474 7
    private function getRunFlags(KeepOptions $keep, $deployMode)
475
    {
476 7
        $flags = Runner::FLAGS;
477 7
        if ($keep->errorKeep) {
478 1
            $flags &= ~Runner::FLAG_KEEP_ON_ERROR;
479 6
        } elseif ($keep->keep) {
480 1
            $flags &= ~(Runner::FLAG_DOCKER_KILL | Runner::FLAG_DOCKER_REMOVE);
481
        }
482
483 7
        if ('copy' === $deployMode) {
484 7
            $flags |= Runner::FLAG_DEPLOY_COPY;
485
        }
486
487 7
        return $flags;
488
    }
489
}
490