Passed
Push — test ( 32b011...971610 )
by Tom
03:24
created

App::parseDockerClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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