Passed
Push — master ( 71d777...1cedb5 )
by Tom
04:21
created

App::parsePrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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