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
![]() |
|||
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 |