Passed
Push — master ( 71afca...9e5fe0 )
by Tom
04:18
created

App::getRunFlags()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 6
nop 2
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 4
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\LibFsPath;
18
use Ktomk\Pipelines\LibFsStream;
19
use Ktomk\Pipelines\Project;
20
use Ktomk\Pipelines\Runner\Directories;
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
        $cache = CacheOptions::bind($args)->run();
114
115 26
        $basename = $this->parseBasename();
116
117 25
        $workingDir = $this->parseWorkingDir();
118
119 24
        $path = $this->parsePath($basename, $workingDir);
120
121 21
        $project = new Project($workingDir);
122
123
        // TODO: obtain project dir information etc. from VCS
124
        // $vcs = new Vcs();
125
126 21
        $runOpts = RunnerOptions::bind($args, $this->streams)->run();
127 18
        $project->setPrefix($runOpts->getPrefix());
128
129 18
        DockerOptions::bind($args, $exec, $project->getPrefix(), $this->streams)->run();
130
131 17
        $noRun = $args->hasOption('no-run');
132
133 17
        $deployMode = $this->parseDeployMode();
134
135 16
        $pipelines = File::createFromFile($path);
136
137 15
        ValidationOptions::bind($args, $this->streams, $pipelines)->run();
138
139 15
        $fileOptions = FileOptions::bind($args, $this->streams, $pipelines)->run();
140
141 11
        $reference = $this->parseReference();
142
143 11
        $env = EnvParser::create($this->arguments)
144 11
            ->parse(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
     * @throws InvalidArgumentException
244
     *
245
     * @return Exec
246
     */
247 27
    private function parseExec()
248
    {
249 27
        $args = $this->arguments;
250
251 27
        $debugPrinter = null;
252 27
        if ($this->verbose) {
253 5
            $debugPrinter = $this->streams;
254
        }
255 27
        $exec = new Exec($debugPrinter);
256
257 27
        if ($args->hasOption('dry-run')) {
258 6
            $exec->setActive(false);
259
        }
260
261 27
        return $exec;
262
    }
263
264
    /**
265
     * @param string $basename
266
     * @param string $workingDir
267
     *
268
     * @throws InvalidArgumentException
269
     * @throws StatusException
270
     * @throws ArgsException
271
     *
272
     * @return string file
273
     */
274 24
    private function parseFile($basename, &$workingDir)
275
    {
276 24
        $args = $this->arguments;
277
278
        /** @var null|string $file as bitbucket-pipelines.yml to process */
279 24
        $file = $args->getOptionArgument('file', null);
280 24
        if (null === $file && null !== $file = LibFs::fileLookUp($basename, $workingDir)) {
281 17
            $buffer = dirname($file);
282 17
            if ($buffer !== $workingDir) {
283 1
                $this->changeWorkingDir($buffer);
284 1
                $workingDir = $this->getWorkingDirectory();
285
            }
286
        }
287
288 24
        if (empty($file)) {
289 1
            throw new StatusException('file can not be empty', 1);
290
        }
291
292 23
        return $file;
293
    }
294
295
    /**
296
     * @param string $basename
297
     * @param string $workingDir
298
     *
299
     * @throws InvalidArgumentException
300
     * @throws ArgsException
301
     * @throws StatusException
302
     *
303
     * @return string path
304
     */
305 24
    private function parsePath($basename, &$workingDir)
306
    {
307 24
        $buffer = (string)$workingDir;
308
309 24
        $file = $this->parseFile($basename, $workingDir);
310
311 23
        $this->verbose(sprintf(
312 23
            'info: project directory is %s',
313 23
            $workingDir === $buffer
314 22
                ? sprintf("'%s'", $workingDir)
315 23
                : sprintf("'%s' (OLDPWD: '%s')", $workingDir, $buffer)
316
        ));
317
318 23
        if ($file !== $basename && self::BBPL_BASENAME !== $basename) {
319 1
            $this->verbose('info: --file overrides non-default --basename');
320
        }
321
322
        // full path to bitbucket-pipelines.yml to process
323 23
        $path = LibFsPath::isAbsolute($file)
324 18
            ? $file
325 23
            : $workingDir . '/' . $file;
326
327
        // support stdin and process substitution for pipelines file
328 23
        if ($file !== LibFsStream::mapFile($file)) {
329 1
            $this->verbose(sprintf('info: reading pipelines from %s', '-' === $file ? 'stdin' : $file));
330
331 1
            return $file;
332
        }
333
334 22
        if (!LibFs::isReadableFile($file)) {
335 2
            throw new StatusException(sprintf('not a readable file: %s', $file), 1);
336
        }
337
338 20
        $this->verbose(sprintf("info: pipelines file is '%s'", $path));
339
340 20
        return $path;
341
    }
342
343
    /**
344
     * @throws InvalidArgumentException
345
     * @throws ArgsException
346
     *
347
     * @return Reference
348
     */
349 11
    private function parseReference()
350
    {
351 11
        $trigger = $this->arguments->getOptionArgument('trigger');
352
353 11
        return Reference::create($trigger);
354
    }
355
356
    /**
357
     * give error about unknown option, show usage and exit status of 1
358
     *
359
     * @throws ArgsException
360
     *
361
     * @return void
362
     */
363 11
    private function parseRemainingOptions()
364
    {
365 11
        $option = $this->arguments->getFirstRemainingOption();
366
367 11
        if ($option) {
368 1
            throw new ArgsException(
369 1
                sprintf('unknown option: %s', $option)
370
            );
371
        }
372 10
    }
373
374
    /**
375
     * @throws InvalidArgumentException
376
     *
377
     * @return Streams
378
     */
379 11
    private function parseStreams()
380
    {
381 11
        $streams = $this->streams;
382
383
        // --verbatim show only errors for own runner actions, show everything from pipeline verbatim
384 11
        if ($this->arguments->hasOption('verbatim')) {
385 1
            $streams = new Streams();
386 1
            $streams->copyHandle($this->streams, 2);
387
        }
388
389 11
        return $streams;
390
    }
391
392
    /**
393
     * @throws InvalidArgumentException
394
     * @throws StatusException
395
     * @throws ArgsException
396
     *
397
     * @return string current working directory
398
     */
399 25
    private function parseWorkingDir()
400
    {
401 25
        $args = $this->arguments;
402
403 25
        $buffer = $args->getStringOptionArgument('working-dir', '');
404
405 25
        if ('' !== $buffer) {
406 2
            $this->changeWorkingDir($buffer);
407
        }
408
409 24
        return $this->getWorkingDirectory();
410
    }
411
412
    /**
413
     * @param string $directory
414
     *
415
     * @throws StatusException
416
     *
417
     * @return void
418
     */
419 2
    private function changeWorkingDir($directory)
420
    {
421 2
        $message = sprintf('changing working directory to %s', $directory);
422 2
        $this->verbose(sprintf('info: %s', $message));
423
424 2
        $result = chdir($directory);
425 2
        if (false === $result) {
426 1
            throw new StatusException(sprintf('fatal: %s', $message), 2);
427
        }
428 1
    }
429
430
    /**
431
     * Obtain pipeline to run from file while handling error output
432
     *
433
     * @param File $pipelines
434
     * @param string $pipelineId
435
     * @param FileOptions $fileOptions
436
     * @param RunOpts $runOpts
437
     *
438
     * @throws ParseException
439
     * @throws StatusException
440
     *
441
     * @return Pipeline on success
442
     */
443 10
    private function getRunPipeline(File $pipelines, $pipelineId, FileOptions $fileOptions, RunOpts $runOpts)
444
    {
445 10
        $this->verbose(sprintf("info: running pipeline '%s'", $pipelineId));
446
447 10
        $pipeline = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $pipeline is dead and can be removed.
Loading history...
448
449
        try {
450 10
            $pipeline = $pipelines->getById($pipelineId);
451 2
        } catch (ParseException $e) {
452 1
            $this->error(sprintf("pipelines: error: pipeline id '%s'", $pipelineId));
453
454 1
            throw $e;
455 1
        } catch (InvalidArgumentException $e) {
456 1
            $this->error(sprintf("pipelines: pipeline '%s' unavailable", $pipelineId));
457 1
            $this->info('Pipelines are:');
458 1
            $fileOptions->showPipelines($pipelines);
459
460 1
            throw new StatusException('', 1);
461
        }
462
463 8
        if (!$pipeline) {
464 1
            throw new StatusException('no pipeline to run!', 1);
465
        }
466
467 7
        $pipeline->setStepsExpression($runOpts->getSteps());
468
469 7
        return $pipeline;
470
    }
471
472
    /**
473
     * @param string $message
474
     *
475
     * @return void
476
     */
477 2
    private function error($message)
478
    {
479 2
        $this->streams->err(
480 2
            sprintf("%s\n", $message)
481
        );
482 2
    }
483
484
    /**
485
     * @param string $message
486
     *
487
     * @return void
488
     */
489 6
    private function info($message)
490
    {
491 6
        $this->streams->out(
492 6
            sprintf("%s\n", $message)
493
        );
494 6
    }
495
496
    /**
497
     * @param string $message
498
     *
499
     * @return void
500
     */
501 24
    private function verbose($message)
502
    {
503 24
        if ($this->verbose) {
504 5
            $this->info($message);
505
        }
506 24
    }
507
}
508