Passed
Push — test ( 4ae916...6f5e5c )
by Tom
02:58
created

App::getRunFlags()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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