Test Failed
Push — test ( 6fb5b7...66cd7f )
by Tom
02:53
created

App::parsePath()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7

Importance

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