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