Passed
Push — master ( 48c4ea...d64961 )
by Tom
02:50
created

App::parsePath()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 8

Importance

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