Runtime::getSSHConfig()   A
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 13
c 1
b 0
f 0
nc 16
nop 0
dl 0
loc 27
ccs 12
cts 12
cp 1
crap 5
rs 9.5222
1
<?php
2
3
/*
4
 * This file is part of the Magallanes package.
5
 *
6
 * (c) Andrés Montañez <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Mage\Runtime;
13
14
use Mage\Deploy\Strategy\ReleasesStrategy;
15
use Mage\Deploy\Strategy\RsyncStrategy;
16
use Mage\Deploy\Strategy\StrategyInterface;
17
use Psr\Log\LoggerInterface;
18
use Psr\Log\LogLevel;
19
use Symfony\Component\Process\Process;
20
use Mage\Runtime\Exception\RuntimeException;
21
use Mage\Task\AbstractTask;
22
23
/**
24
 * Runtime is a container of all run in time configuration, stages of progress, hosts being deployed, etc.
25
 *
26
 * @author Andrés Montañez <[email protected]>
27
 */
28
class Runtime
29
{
30
    public const PRE_DEPLOY = 'pre-deploy';
31
    public const ON_DEPLOY = 'on-deploy';
32
    public const POST_DEPLOY = 'post-deploy';
33
    public const ON_RELEASE = 'on-release';
34
    public const POST_RELEASE = 'post-release';
35
36
    /**
37
     * @var array<string, mixed> Magallanes configuration
38
     */
39
    protected array $configuration = [];
40
41
    /**
42
     * @var string|null Environment being deployed
43
     */
44
    protected ?string $environment = null;
45
46
    /**
47
     * @var string|null Stage of Deployment
48
     */
49
    protected ?string $stage = null;
50
51
    /**
52
     * @var string|null The host being deployed to
53
     */
54
    protected ?string $workingHost = null;
55
56
    /**
57
     * @var string|null The Release ID
58
     */
59
    protected ?string $releaseId = null;
60
61
    /**
62
     * @var array<string, string> Hold a bag of variables for sharing information between tasks, if needed
63
     */
64
    protected $vars = [];
65
66
    protected ?LoggerInterface $logger = null;
67
68
    /**
69
     * @var bool Indicates if a Rollback operation is in progress
70
     */
71
    protected bool $rollback = false;
72
73 16
    public function isWindows(): bool
74
    {
75 16
        return stripos(PHP_OS, 'WIN') === 0;
76
    }
77
78 39
    public function hasPosix(): bool
79
    {
80 39
        return function_exists('posix_getpwuid');
81
    }
82
83
    /**
84
     * Generate the Release ID
85
     */
86 1
    public function generateReleaseId(): self
87
    {
88 1
        $this->setReleaseId(date('YmdHis'));
89 1
        return $this;
90
    }
91
92
    /**
93
     * Sets the Release ID
94
     */
95 22
    public function setReleaseId(string $releaseId): self
96
    {
97 22
        $this->releaseId = $releaseId;
98 22
        return $this;
99
    }
100
101
    /**
102
     * Retrieve the current Release ID
103
     */
104 52
    public function getReleaseId(): ?string
105
    {
106 52
        return $this->releaseId;
107
    }
108
109
    /**
110
     * Sets the Runtime in Rollback mode On or Off
111
     */
112 1
    public function setRollback(bool $inRollback): self
113
    {
114 1
        $this->rollback = $inRollback;
115 1
        return $this;
116
    }
117
118
    /**
119
     * Indicates if Runtime is in rollback
120
     */
121 39
    public function inRollback(): bool
122
    {
123 39
        return $this->rollback;
124
    }
125
126
    /**
127
     * Sets a value in the Vars bag
128
     */
129 30
    public function setVar(string $key, string $value): self
130
    {
131 30
        $this->vars[$key] = $value;
132 30
        return $this;
133
    }
134
135
    /**
136
     * Retrieve a value from the Vars bag, or a default (null) if not set
137
     */
138 32
    public function getVar(string $key, mixed $default = null): ?string
139
    {
140 32
        if (array_key_exists($key, $this->vars)) {
141 30
            return $this->vars[$key];
142
        }
143
144 32
        return $default;
145
    }
146
147
    /**
148
     * Sets the Logger instance
149
     */
150 57
    public function setLogger(?LoggerInterface $logger = null): self
151
    {
152 57
        $this->logger = $logger;
153 57
        return $this;
154
    }
155
156
    /**
157
     * Sets the Magallanes Configuration to the Runtime
158
     *
159
     * @param array<string, mixed> $configuration
160
     */
161 90
    public function setConfiguration(array $configuration): self
162
    {
163 90
        $this->configuration = $configuration;
164 90
        return $this;
165
    }
166
167
    /**
168
     * Retrieve the Configuration
169
     *
170
     * @return array<string, mixed> $configuration
171
     */
172 1
    public function getConfiguration(): array
173
    {
174 1
        return $this->configuration;
175
    }
176
177
    /**
178
     * Retrieves the Configuration Option for a specific section in the configuration
179
     */
180 56
    public function getConfigOption(string $key, mixed $default = null): mixed
181
    {
182 56
        if (array_key_exists($key, $this->configuration)) {
183 50
            return $this->configuration[$key];
184
        }
185
186 47
        return $default;
187
    }
188
189
    /**
190
     * Returns the Configuration Option for a specific section the current Environment
191
     */
192 59
    public function getEnvOption(string $key, mixed $default = null): mixed
193
    {
194
        if (
195 59
            !array_key_exists('environments', $this->configuration) ||
196 59
            !is_array($this->configuration['environments'])
197
        ) {
198 2
            return $default;
199
        }
200
201 57
        if (!array_key_exists($this->environment, $this->configuration['environments'])) {
202 1
            return $default;
203
        }
204
205 56
        if (array_key_exists($key, $this->configuration['environments'][$this->environment])) {
206 48
            return $this->configuration['environments'][$this->environment][$key];
207
        }
208
209 53
        return $default;
210
    }
211
212
    /**
213
     * Shortcut to get the the configuration option for a specific environment and merge it with
214
     * the global one (environment specific overrides the global one if present).
215
     *
216
     * @param array<string, mixed> $defaultEnv
217
     * @return array<string, mixed>
218
     */
219 36
    public function getMergedOption(string $key, array $defaultEnv = []): array
220
    {
221 36
        $userGlobalOptions = $this->getConfigOption($key, $defaultEnv);
222 36
        $userEnvOptions = $this->getEnvOption($key, $defaultEnv);
223
224 36
        return array_merge(
225 36
            (is_array($userGlobalOptions) ? $userGlobalOptions : []),
226 36
            (is_array($userEnvOptions) ? $userEnvOptions : [])
227
        );
228
    }
229
230
    /**
231
     * Overwrites an Environment Configuration Option
232
     */
233 2
    public function setEnvOption(string $key, mixed $value): self
234
    {
235 2
        if (array_key_exists('environments', $this->configuration) && is_array($this->configuration['environments'])) {
236 2
            if (array_key_exists($this->environment, $this->configuration['environments'])) {
237 2
                $this->configuration['environments'][$this->environment][$key] = $value;
238
            }
239
        }
240
241 2
        return $this;
242
    }
243
244
    /**
245
     * Sets the working Environment
246
     *
247
     * @throws RuntimeException
248
     */
249 88
    public function setEnvironment(string $environment): self
250
    {
251
        if (
252 88
            array_key_exists('environments', $this->configuration) &&
253 88
            array_key_exists($environment, $this->configuration['environments'])
254
        ) {
255 85
            $this->environment = $environment;
256 85
            return $this;
257
        }
258
259 3
        throw new RuntimeException(sprintf('The environment "%s" does not exists.', $environment), 100);
260
    }
261
262
    /**
263
     * Returns the current working Environment
264
     */
265 70
    public function getEnvironment(): ?string
266
    {
267 70
        return $this->environment;
268
    }
269
270
    /**
271
     * Sets the working stage
272
     */
273 42
    public function setStage(string $stage): self
274
    {
275 42
        $this->stage = $stage;
276 42
        return $this;
277
    }
278
279
    /**
280
     * Retrieve the current working Stage
281
     */
282 62
    public function getStage(): ?string
283
    {
284 62
        return $this->stage;
285
    }
286
287
    /**
288
     * Retrieve the defined Tasks for the current Environment and Stage
289
     *
290
     * @return string[]
291
     */
292 42
    public function getTasks(): array
293
    {
294
        if (
295 42
            !array_key_exists('environments', $this->configuration) ||
296 42
            !is_array($this->configuration['environments'])
297
        ) {
298 1
            return [];
299
        }
300
301 41
        if (!array_key_exists($this->environment, $this->configuration['environments'])) {
302 1
            return [];
303
        }
304
305 40
        if (array_key_exists($this->stage, $this->configuration['environments'][$this->environment])) {
306 39
            if (is_array($this->configuration['environments'][$this->environment][$this->stage])) {
307 39
                return $this->configuration['environments'][$this->environment][$this->stage];
308
            }
309
        }
310
311 30
        return [];
312
    }
313
314
    /**
315
     * Sets the working Host
316
     */
317 41
    public function setWorkingHost(?string $host): self
318
    {
319 41
        $this->workingHost = $host;
320 41
        return $this;
321
    }
322
323
    /**
324
     * Retrieve the working Host
325
     */
326 70
    public function getWorkingHost(): ?string
327
    {
328 70
        return $this->workingHost;
329
    }
330
331
    /**
332
     * Logs a Message into the Logger
333
     */
334 48
    public function log(string $message, string $level = LogLevel::DEBUG): void
335
    {
336 48
        if ($this->logger instanceof LoggerInterface) {
337 46
            $this->logger->log($level, $message);
338
        }
339
    }
340
341
    /**
342
     * Executes a command, it will be run Locally or Remotely based on the working Stage
343
     */
344 50
    public function runCommand(string $cmd, int $timeout = 120): Process
345
    {
346 50
        switch ($this->getStage()) {
347
            case self::ON_DEPLOY:
348
            case self::ON_RELEASE:
349
            case self::POST_RELEASE:
350 20
                return $this->runRemoteCommand($cmd, true, $timeout);
351
            default:
352 48
                return $this->runLocalCommand($cmd, $timeout);
353
        }
354
    }
355
356
    /**
357
     * Execute a command locally
358
     */
359 1
    public function runLocalCommand(string $cmd, int $timeout = 120): Process
360
    {
361 1
        $this->log($cmd, LogLevel::INFO);
362
363 1
        $process = Process::fromShellCommandline($cmd);
364 1
        $process->setTimeout($timeout);
365 1
        $process->run();
366
367 1
        $this->log($process->getOutput(), LogLevel::DEBUG);
368 1
        if (!$process->isSuccessful()) {
369 1
            $this->log($process->getErrorOutput(), LogLevel::ERROR);
370
        }
371
372 1
        return $process;
373
    }
374
375
    /**
376
     * Executes a command remotely, if jail is true, it will run inside the Host Path and the Release (if available)
377
     */
378 30
    public function runRemoteCommand(string $cmd, bool $jail, int $timeout = 120): Process
379
    {
380 30
        $user = $this->getEnvOption('user', $this->getCurrentUser());
381 30
        $sudo = $this->getEnvOption('sudo', false);
382 30
        $host = $this->getHostName();
383 30
        $sshConfig = $this->getSSHConfig();
384
385 30
        $cmdDelegate = $cmd;
386 30
        if ($sudo === true) {
387 1
            $cmdDelegate = sprintf('sudo %s', $cmd);
388
        }
389
390 30
        $hostPath = rtrim($this->getEnvOption('host_path'), '/');
391 30
        if ($jail && $this->getReleaseId() !== null) {
392 12
            $cmdDelegate = sprintf('cd %s/releases/%s && %s', $hostPath, $this->getReleaseId(), $cmdDelegate);
393 30
        } elseif ($jail) {
394 8
            $cmdDelegate = sprintf('cd %s && %s', $hostPath, $cmdDelegate);
395
        }
396
397 30
        $cmdRemote = str_replace('"', '\"', $cmdDelegate);
398 30
        $cmdLocal = sprintf(
399
            'ssh -p %d %s %s@%s "%s"',
400 30
            $sshConfig['port'],
401 30
            $sshConfig['flags'],
402
            $user,
403
            $host,
404
            $cmdRemote
405
        );
406
407 30
        return $this->runLocalCommand($cmdLocal, $timeout);
408
    }
409
410
    /**
411
     * Get the SSH configuration based on the environment
412
     *
413
     * @return array<string, string>
414
     */
415 43
    public function getSSHConfig(): array
416
    {
417 43
        $sshConfig = $this->getEnvOption(
418
            'ssh',
419
            [
420 43
                'port' => 22,
421
                'flags' => '-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
422
            ]
423
        );
424
425 43
        if ($this->getHostPort() !== null) {
426 3
            $sshConfig['port'] = $this->getHostPort();
427
        }
428
429 43
        if (!array_key_exists('port', $sshConfig)) {
430 2
            $sshConfig['port'] = '22';
431
        }
432
433 43
        if (!array_key_exists('flags', $sshConfig)) {
434 3
            $sshConfig['flags'] = '-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no';
435
        }
436
437 43
        if (!array_key_exists('timeout', $sshConfig)) {
438 42
            $sshConfig['timeout'] = 300;
439
        }
440
441 43
        return $sshConfig;
442
    }
443
444
    /**
445
     * Get the current Host Port or default ssh port
446
     */
447 43
    public function getHostPort(): ?int
448
    {
449 43
        $info = explode(':', strval($this->getWorkingHost()));
450 43
        return isset($info[1]) ? intval($info[1]) : null;
451
    }
452
453
    /**
454
     * Get the current Host Name
455
     */
456 66
    public function getHostName(): ?string
457
    {
458 66
        if (strpos(strval($this->getWorkingHost()), ':') === false) {
459 66
            return $this->getWorkingHost();
460
        }
461
462 2
        $info = explode(':', $this->getWorkingHost());
0 ignored issues
show
Bug introduced by
It seems like $this->getWorkingHost() can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

462
        $info = explode(':', /** @scrutinizer ignore-type */ $this->getWorkingHost());
Loading history...
463 2
        return strval($info[0]);
464
    }
465
466
    /**
467
     * Gets a Temporal File name
468
     */
469 1
    public function getTempFile(): string
470
    {
471 1
        return tempnam(sys_get_temp_dir(), 'mage');
472
    }
473
474
    /**
475
     * Get the current user
476
     */
477 40
    public function getCurrentUser(): string
478
    {
479 40
        if ($this->hasPosix()) {
480 39
            $userData = posix_getpwuid(posix_geteuid());
481 39
            return $userData['name'];
482
        }
483
484
        // Windows fallback
485 1
        return strval(getenv('USERNAME'));
486
    }
487
488
    /**
489
     * Shortcut for getting Branch information
490
     *
491
     * @return bool|string
492
     */
493 40
    public function getBranch(): mixed
494
    {
495 40
        return $this->getEnvOption('branch', false);
496
    }
497
498
    /**
499
     * Shortcut for getting Tag information
500
     *
501
     * @return bool|string
502
     */
503 8
    public function getTag(): mixed
504
    {
505 8
        return $this->getEnvOption('tag', false);
506
    }
507
508
    /**
509
     * Guesses the Deploy Strategy to use
510
     */
511 46
    public function guessStrategy(): StrategyInterface
512
    {
513 46
        $strategy = new RsyncStrategy();
514
515 46
        if ($this->getEnvOption('releases', false)) {
516 20
            $strategy = new ReleasesStrategy();
517
        }
518
519 46
        $strategy->setRuntime($this);
520 46
        return $strategy;
521
    }
522
}
523