TikiWiki/tiki-manager

View on GitHub
src/Command/UpdateInstanceCommand.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * @copyright (c) Copyright by authors of the Tiki Manager Project. All Rights Reserved.
 *     See copyright.txt for details and a complete list of authors.
 * @licence Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See LICENSE for details.
 */

namespace TikiManager\Command;

use Monolog\Formatter\LineFormatter;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use TikiManager\Application\Instance;
use TikiManager\Application\Version;
use TikiManager\Command\Helper\CommandHelper;
use TikiManager\Command\Traits\InstanceUpgrade;
use TikiManager\Command\Traits\SendEmail;
use TikiManager\Libs\Helpers\Checksum;
use TikiManager\Logger\ArrayHandler;

class UpdateInstanceCommand extends TikiManagerCommand
{
    use InstanceUpgrade;
    use SendEmail;

    protected function configure()
    {
        parent::configure();

        $this
            ->setName('instance:update')
            ->setDescription('Update instance')
            ->setHelp('This command allows you update an instance')
            ->addArgument('mode', InputArgument::IS_ARRAY | InputArgument::OPTIONAL)
            ->addOption(
                'instances',
                'i',
                InputOption::VALUE_OPTIONAL,
                'List of instance IDs to be updated, separated by comma (,)'
            )
            ->addOption(
                'branch',
                'b',
                InputOption::VALUE_REQUIRED,
                'Instance branch to update'
            )
            ->addOption(
                'check',
                null,
                InputOption::VALUE_NONE,
                'Check files checksum after operation has been performed.'
            )
            ->addOption(
                'email',
                'e',
                InputOption::VALUE_REQUIRED,
                'Email address to notify in case of failure. Use , (comma) to separate multiple email addresses.'
            )
            ->addOption(
                'skip-reindex',
                null,
                InputOption::VALUE_NONE,
                'Skip rebuilding index step.'
            )
            ->addOption(
                'skip-cache-warmup',
                null,
                InputOption::VALUE_NONE,
                'Skip generating cache step.'
            )
            ->addOption(
                'live-reindex',
                null,
                InputOption::VALUE_OPTIONAL,
                'Live reindex, set instance maintenance off and after perform index rebuild.',
                true
            )
            ->addOption(
                'lag',
                null,
                InputOption::VALUE_REQUIRED,
                'Time delay commits by X number of days. Useful for avoiding newly introduced bugs in automated updates.'
            )
            ->addOption(
                'stash',
                null,
                InputOption::VALUE_NONE,
                'Saves your local modifications, and try to apply after update/upgrade'
            )
            ->addOption(
                'ignore-requirements',
                null,
                InputOption::VALUE_NONE,
                'Ignore version requirements. Allows to select non-supported branches, useful for testing.'
            )
            ->addOption(
                'no-maintenance',
                null,
                InputOption::VALUE_NONE,
                'Update will be performed without setting the website in maintenance mode.'
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $instances = CommandHelper::getInstances('update');
        $instancesInfo = CommandHelper::getInstancesInfo($instances);
        $enableMaintenance = empty($input->getOption('no-maintenance'));

        if (isset($instancesInfo)) {
            $instancesOption = $input->getOption('instances');

            $auto = false;
            $switch = false;

            $argument = $input->getArgument('mode');
            if (isset($argument) && !empty($argument)) {
                if (is_array($argument)) {
                    $auto = $input->getArgument('mode')[0] == 'auto';
                    $switch = $input->getArgument('mode')[0] == 'switch';
                } else {
                    $switch = $input->getArgument('mode') == 'switch';
                }
            }

            if ($auto) {
                $instancesIds = array_slice($input->getArgument('mode'), 1);

                $selectedInstances = [];
                foreach ($instancesIds as $index) {
                    if (array_key_exists($index, $instances)) {
                        $selectedInstances[] = $instances[$index];
                    }
                }
            } else {
                $action = 'update';
                if ($switch) {
                    $action = 'upgrade';
                }

                if (empty($instancesOption)) {
                    $this->io->newLine();
                    CommandHelper::renderInstancesTable($output, $instancesInfo);

                    $this->io->newLine();
                    $this->io->writeln('<comment>In case you want to ' . $action . ' more than one instance, please use a comma (,) between the values</comment>');

                    $selectedInstances = $this->io->ask(
                        'Which instance(s) do you want to ' . $action . '?',
                        null,
                        function ($answer) use ($instances) {
                            return CommandHelper::validateInstanceSelection($answer, $instances);
                        }
                    );
                } else {
                    CommandHelper::validateInstanceSelection($instancesOption, $instances);
                    $instancesOption = explode(',', $instancesOption);
                    $selectedInstances = array_intersect_key($instances, array_flip($instancesOption));
                }
            }

            $lag = $input->getOption('lag');
            if ($lag && (!is_numeric($lag) || $lag < 0)) {
                $this->io->error('Invalid option for --lag, must be a positive integer.');
                return 1;
            }

            $checksumCheck = $input->getOption('check');
            $skipReindex = $input->getOption('skip-reindex');
            $skipCache = $input->getOption('skip-cache-warmup');
            $liveReindex = filter_var($input->getOption('live-reindex'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? true;
            $logs = [];

            $vcsOptions = [
                'allow_stash' => $input->getOption('stash')
            ];

            $hookName = $this->getCommandHook();
            /** @var Instance $instance */
            foreach ($selectedInstances as $instance) {
                $instanceLogger = $this->logger->withName('instance_' . $instance->id);
                $arrHandler = new ArrayHandler(Logger::ERROR);
                $arrHandler->setFormatter($this->getFormatter());
                $instanceLogger->pushHandler($arrHandler);

                $instanceVCS = $instance->getVersionControlSystem();
                $instanceVCS->setLogger($instanceLogger);
                $instanceVCS->setVCSOptions($vcsOptions);

                // Ensure that the current phpexec is still valid;
                $instance->detectPHP();
                $phpVersion = CommandHelper::formatPhpVersion($instance->phpversion);

                $message = sprintf(
                    "Working on %s\nPHP version %s found at %s.",
                    $instance->name,
                    $phpVersion,
                    $instance->phpexec
                );
                $this->io->writeln('<fg=cyan>' .$message. '</>');

                if ($enableMaintenance) {
                    $instance->lock();
                }

                $app = $instance->getApplication();
                $version = $instance->getLatestVersion();
                $branch_name = $version->getBranch();

                $hookName->registerPostHookVars(['instance' => $instance]);

                $options = [
                    'checksum-check' => $checksumCheck,
                    'skip-reindex' => $skipReindex,
                    'skip-cache-warmup' => $skipCache,
                    'live-reindex' => $liveReindex,
                    'lag' => $lag,
                ];

                if ($switch) {
                    $this->runUpgrade($instance, $options, $instanceLogger);
                } else {
                    $app_branch = $app->getBranch();
                    if ($app_branch == $branch_name) {
                        $this->runUpdate($instance, $options, $instanceLogger);
                    } else {
                        $message = 'Tiki Application branch is different than the one stored in the Tiki Manager db.';
                        $instanceLogger->error($message);
                    }
                }

                if ($instanceLogs = $arrHandler->getLog()) {
                    $logs[] = sprintf('<h1>%s (id: %s)</h1>', $instance->name, $instance->id);
                    $logs[] = '<pre>' . implode("\n", $instanceLogs) . '</pre>';
                }

                if ($instance->isLocked()) {
                    $instance->unlock();
                }
            }

            $emails = $input->getOption('email') ?? '';
            $emails = array_filter(explode(',', $emails), function ($email) {
                return filter_var(trim($email), FILTER_VALIDATE_EMAIL);
            });

            if (!empty($logs) && !empty($emails)) {
                $logs = implode(PHP_EOL, $logs);
                try {
                    $this->sendEmail(
                        $emails,
                        '[Tiki-Manager] ' . $this->getName() . ' report failures',
                        $logs
                    );
                } catch (\RuntimeException $e) {
                    debug($e->getMessage());
                    $this->io->error($e->getMessage());
                }
            }

            if (!empty($logs)) {
                return 1;
            }
        } else {
            $this->io->writeln('<comment>No instances available to update/upgrade.</comment>');
        }

        return 0;
    }

    private function getFormatter()
    {
        $formatter = new LineFormatter();
        $formatter->ignoreEmptyContextAndExtra(true);
        $formatter->allowInlineLineBreaks(true);
        $formatter->includeStacktraces(true);

        return $formatter;
    }

    protected function runUpdate(Instance $instance, array $options, LoggerInterface $logger)
    {
        try {
            $app = $instance->getApplication();
            $filesToResolve = $app->performUpdate($instance, null, $options);
            $version = $instance->getLatestVersion();

            if ($options['checksum-check'] ?? false) {
                Checksum::handleCheckResult($instance, $version, $filesToResolve);
            }
        } catch (\Exception $e) {
            $logger->error('Failed to update instance!', [
                'instance' => $instance->name,
                'exception' => $e,
            ]);
            CommandHelper::setInstanceSetupError($instance->id, $e);
        }
    }

    protected function runUpgrade(Instance $instance, array $options, LoggerInterface $logger)
    {
        $branch = $this->input->getOption('branch');
        $skipRequirements = $this->input->getOption('ignore-requirements') ?? false;
        $selectedVersion = $this->getUpgradeVersion($instance, !$skipRequirements, $branch);

        $target = Version::buildFake($instance->vcs_type, $selectedVersion);

        try {
            $app = $instance->getApplication();
            $filesToResolve = $app->performUpdate($instance, $target, $options);
            $version = $instance->getLatestVersion();

            if ($options['checksum-check'] ?? false) {
                Checksum::handleCheckResult($instance, $version, $filesToResolve);
            }
        } catch (\Exception $e) {
            $logger->error('Failed to upgrade instance!', [
                'instance' => $instance->name,
                'exception' => $e,
            ]);
            CommandHelper::setInstanceSetupError($instance->id, $e);
        }
    }
}