Passed
Push — test ( 66cd7f...018ff3 )
by Tom
04:25
created

App::parseReference()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
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\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 30
    public function __construct(Streams $streams)
63
    {
64 30
        $this->streams = $streams;
65 30
        $this->help = new Help($streams);
66 30
    }
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 29
    public function main(array $arguments)
76
    {
77 29
        $args = Args::create($arguments);
78
79 29
        $this->verbose = $args->hasOption(array('v', 'verbose'));
80 29
        $this->arguments = $args;
81
82 29
        $handler = new ExceptionHandler(
83 29
            $this->streams,
84 29
            $this->help,
85 29
            $args->hasOption('debug')
86
        );
87
88 29
        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 29
    public function run()
102
    {
103 29
        $args = $this->arguments;
104
105 29
        $this->help->run($args);
106
107 27
        $exec = $this->parseExec();
108
109 27
        $keep = KeepOptions::bind($args)->run();
110
111 26
        $basename = $this->parseBasename();
112
113 25
        $workingDir = $this->parseWorkingDir();
114
115 24
        $path = $this->parsePath($basename, $workingDir);
116
117
        // TODO: obtain project dir information etc. from VCS
118
        // $vcs = new Vcs();
119
120 21
        $runOpts = RunnerOptions::bind($args, $this->streams)->run();
121
122 18
        DockerOptions::bind($args, $exec, $runOpts->getPrefix(), $this->streams)->run();
123
124 17
        $noRun = $args->hasOption('no-run');
125
126 17
        $deployMode = $this->parseDeployMode();
127
128 16
        $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 24
    private function getWorkingDirectory()
168
    {
169 24
        $workingDir = \getcwd();
170 24
        if (false === $workingDir) {
171
            // @codeCoverageIgnoreStart
172
            throw new StatusException('fatal: obtain working directory', 1);
173
            // @codeCoverageIgnoreEnd
174
        }
175
176 24
        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 26
    private function parseBasename()
187
    {
188 26
        $args = $this->arguments;
189
190 26
        $basename = $args->getStringOptionArgument('basename', self::BBPL_BASENAME);
191 26
        if (!LibFs::isBasename($basename)) {
192 1
            throw new StatusException(sprintf("not a basename: '%s'", $basename), 1);
193
        }
194
195 25
        return $basename;
196
    }
197
198
    /**
199
     * @throws InvalidArgumentException
200
     * @throws ArgsException
201
     * @throws StatusException
202
     *
203
     * @return string deploy mode ('copy', 'mount')
204
     */
205 17
    private function parseDeployMode()
206
    {
207 17
        $args = $this->arguments;
208
209 17
        $deployMode = $args->getStringOptionArgument('deploy', 'copy');
210
211 17
        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 16
        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 27
    private function parseExec()
266
    {
267 27
        $args = $this->arguments;
268
269 27
        $debugPrinter = null;
270 27
        if ($this->verbose) {
271 5
            $debugPrinter = $this->streams;
272
        }
273 27
        $exec = new Exec($debugPrinter);
274
275 27
        if ($args->hasOption('dry-run')) {
276 6
            $exec->setActive(false);
277
        }
278
279 27
        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 24
    private function parseFile($basename, &$workingDir)
293
    {
294 24
        $args = $this->arguments;
295
296
        /** @var null|string $file as bitbucket-pipelines.yml to process */
297 24
        $file = $args->getOptionArgument('file', null);
298 24
        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 24
        if (empty($file)) {
307 1
            throw new StatusException('file can not be empty', 1);
308
        }
309
310 23
        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 24
    private function parsePath($basename, &$workingDir)
324
    {
325 24
        $buffer = (string)$workingDir;
326
327 24
        $file = $this->parseFile($basename, $workingDir);
328
329 23
        $this->verbose(sprintf(
330 23
            'info: project directory is %s',
331 23
            $workingDir === $buffer
332 22
                ? sprintf("'%s'", $workingDir)
333 23
                : sprintf("'%s' (OLDPWD: '%s')", $workingDir, $buffer)
334
        ));
335
336 23
        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 23
        $path = LibFs::isAbsolutePath($file)
342 18
            ? $file
343 23
            : $workingDir . '/' . $file;
344
345
        // support stdin for pipelines file
346 23
        if ('-' === $file) {
347 1
            $this->verbose('info: reading pipelines from stdin');
348
349 1
            return $file;
350
        }
351
352 22
        if (!LibFs::isReadableFile($file)) {
353 2
            throw new StatusException(sprintf('not a readable file: %s', $file), 1);
354
        }
355
356 20
        $this->verbose(sprintf("info: pipelines file is '%s'", $path));
357
358 20
        return $path;
359
    }
360
361
    /**
362
     * @throws InvalidArgumentException
363
     * @throws ArgsException
364
     *
365
     * @return Reference
366
     */
367 11
    private function parseReference()
368
    {
369 11
        $trigger = $this->arguments->getOptionArgument('trigger');
370
371 11
        return Reference::create($trigger);
372
    }
373
374
    /**
375
     * give error about unknown option, show usage and exit status of 1
376
     *
377
     * @throws ArgsException
378
     *
379
     * @return void
380
     */
381 11
    private function parseRemainingOptions()
382
    {
383 11
        $option = $this->arguments->getFirstRemainingOption();
384
385 11
        if ($option) {
386 1
            ArgsException::__(
387 1
                sprintf('unknown option: %s', $option)
388
            );
389
        }
390 10
    }
391
392
    /**
393
     * @throws InvalidArgumentException
394
     *
395
     * @return Streams
396
     */
397 11
    private function parseStreams()
398
    {
399 11
        $streams = $this->streams;
400
401
        // --verbatim show only errors for own runner actions, show everything from pipeline verbatim
402 11
        if ($this->arguments->hasOption('verbatim')) {
403 1
            $streams = new Streams();
404 1
            $streams->copyHandle($this->streams, 2);
405
        }
406
407 11
        return $streams;
408
    }
409
410
    /**
411
     * @throws InvalidArgumentException
412
     * @throws StatusException
413
     * @throws ArgsException
414
     *
415
     * @return string current working directory
416
     */
417 25
    private function parseWorkingDir()
418
    {
419 25
        $args = $this->arguments;
420
421 25
        $buffer = $args->getStringOptionArgument('working-dir', '');
422
423 25
        if ('' !== $buffer) {
424 2
            $this->changeWorkingDir($buffer);
425
        }
426
427 24
        return $this->getWorkingDirectory();
428
    }
429
430
    /**
431
     * @param string $directory
432
     *
433
     * @throws StatusException
434
     *
435
     * @return void
436
     */
437 2
    private function changeWorkingDir($directory)
438
    {
439 2
        $message = sprintf('changing working directory to %s', $directory);
440 2
        $this->verbose(sprintf('info: %s', $message));
441
442 2
        $result = chdir($directory);
443 2
        if (false === $result) {
444 1
            throw new StatusException(sprintf('fatal: %s', $message), 2);
445
        }
446 1
    }
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
     * @param RunOpts $runOpts
455
     *
456
     * @throws ParseException
457
     * @throws StatusException
458
     *
459
     * @return Pipeline on success
460
     */
461 10
    private function getRunPipeline(File $pipelines, $pipelineId, FileOptions $fileOptions, RunOpts $runOpts)
462
    {
463 10
        $this->verbose(sprintf("info: running pipeline '%s'", $pipelineId));
464
465 10
        $pipeline = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pipeline is dead and can be removed.
Loading history...
466
467
        try {
468 10
            $pipeline = $pipelines->getById($pipelineId);
469 2
        } catch (ParseException $e) {
470 1
            $this->error(sprintf("pipelines: error: pipeline id '%s'", $pipelineId));
471
472 1
            throw $e;
473 1
        } catch (InvalidArgumentException $e) {
474 1
            $this->error(sprintf("pipelines: pipeline '%s' unavailable", $pipelineId));
475 1
            $this->info('Pipelines are:');
476 1
            $fileOptions->showPipelines($pipelines);
477
478 1
            throw new StatusException('', 1);
479
        }
480
481 8
        if (!$pipeline) {
482 1
            throw new StatusException('no pipeline to run!', 1);
483
        }
484
485 7
        $pipeline->setStepsExpression($runOpts->getSteps());
486
487 7
        return $pipeline;
488
    }
489
490
    /**
491
     * @param string $message
492
     *
493
     * @return void
494
     */
495 2
    private function error($message)
496
    {
497 2
        $this->streams->err(
498 2
            sprintf("%s\n", $message)
499
        );
500 2
    }
501
502
    /**
503
     * @param string $message
504
     *
505
     * @return void
506
     */
507 6
    private function info($message)
508
    {
509 6
        $this->streams->out(
510 6
            sprintf("%s\n", $message)
511
        );
512 6
    }
513
514
    /**
515
     * @param string $message
516
     *
517
     * @return void
518
     */
519 24
    private function verbose($message)
520
    {
521 24
        if ($this->verbose) {
522 5
            $this->info($message);
523
        }
524 24
    }
525
526
    /**
527
     * Map diverse parameters to run flags
528
     *
529
     * @param KeepOptions $keep
530
     * @param string $deployMode
531
     *
532
     * @return Flags
533
     */
534 7
    private function getRunFlags(KeepOptions $keep, $deployMode)
535
    {
536 7
        $flagsValue = Flags::FLAGS;
537 7
        if ($keep->errorKeep) {
538 1
            $flagsValue |= Flags::FLAG_KEEP_ON_ERROR;
539 6
        } elseif ($keep->keep) {
540 1
            $flagsValue &= ~(Flags::FLAG_DOCKER_KILL | Flags::FLAG_DOCKER_REMOVE);
541
        }
542
543 7
        if ('copy' === $deployMode) {
544 7
            $flagsValue |= Flags::FLAG_DEPLOY_COPY;
545
        }
546
547 7
        return new Flags($flagsValue);
548
    }
549
}
550