diff --git a/bin/froxlor-cli b/bin/froxlor-cli index eb5e214e..6301a426 100755 --- a/bin/froxlor-cli +++ b/bin/froxlor-cli @@ -26,6 +26,7 @@ declare(strict_types=1); +use Froxlor\Cli\ConfigDiff; use Symfony\Component\Console\Application; use Froxlor\Cli\RunApiCommand; use Froxlor\Cli\ConfigServices; @@ -61,4 +62,5 @@ $application->add(new InstallCommand()); $application->add(new MasterCron()); $application->add(new UserCommand()); $application->add(new ValidateAcmeWebroot()); +$application->add(new ConfigDiff()); $application->run(); diff --git a/lib/Froxlor/Cli/ConfigDiff.php b/lib/Froxlor/Cli/ConfigDiff.php new file mode 100644 index 00000000..590c7faf --- /dev/null +++ b/lib/Froxlor/Cli/ConfigDiff.php @@ -0,0 +1,178 @@ + + * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 + */ + +namespace Froxlor\Cli; + +use Froxlor\Config\ConfigParser; +use Froxlor\FileDir; +use Froxlor\Froxlor; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +final class ConfigDiff extends CliCommand +{ + protected function configure(): void + { + $this->setName('froxlor:config-diff') + ->setDescription('Shows differences in config templates between OS versions') + ->addArgument('from', InputArgument::OPTIONAL, 'OS version to compare against') + ->addArgument('to', InputArgument::OPTIONAL, 'OS version to compare from') + ->addOption('list', 'l', InputOption::VALUE_NONE, 'List all possible OS versions') + ->addOption('diff-params', 'p', InputOption::VALUE_REQUIRED, 'Additional parameters for `diff`'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + require Froxlor::getInstallDir() . '/lib/functions.php'; + + $parsers = $versions = []; + foreach (glob(Froxlor::getInstallDir() . '/lib/configfiles/*.xml') as $config) { + $name = str_replace(".xml", "", strtolower(basename($config))); + $parser = new ConfigParser($config); + $versions[$name] = $parser->getCompleteDistroName(); + $parsers[$name] = $parser; + } + asort($versions); + + if ($input->getOption('list') === true) { + $output->writeln('The following OS version templates are available:'); + foreach ($versions as $k => $v) { + $output->writeln(str_pad($k, 20) . $v); + } + return self::SUCCESS; + } + + if (!$input->hasArgument('from') || !array_key_exists($input->getArgument('from'), $versions)) { + $output->writeln('Missing or invalid "from" argument.'); + $output->writeln('Available versions: ' . implode(', ', array_keys($versions))); + return self::INVALID; + } + + if (!$input->hasArgument('to') || !array_key_exists($input->getArgument('to'), $versions)) { + $output->writeln('Missing or invalid "to" argument.'); + $output->writeln('Available versions: ' . implode(', ', array_keys($versions))); + return self::INVALID; + } + + // Make sure diff is installed + $check_diff_installed = FileDir::safe_exec('which diff'); + if (count($check_diff_installed) === 0) { + $output->writeln('Unable to find "diff" installation on your system.'); + return self::INVALID; + } + + $parser_from = $parsers[$input->getArgument('from')]; + $parser_to = $parsers[$input->getArgument('to')]; + $tmp_from = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_from'); + $tmp_to = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_to'); + $files = []; + $titles_by_key = []; + + // Aggregate content for each config file + foreach ([[$parser_from, 'from'], [$parser_to, 'to']] as $todo) { + foreach ($todo[0]->getServices() as $service_type => $service) { + foreach ($service->getDaemons() as $daemon_name => $daemon) { + foreach ($daemon->getConfig() as $instruction) { + if ($instruction['type'] !== 'file') { + continue; + } + + if (isset($instruction['subcommands'])) { + foreach ($instruction['subcommands'] as $subinstruction) { + if ($subinstruction['type'] !== 'file') { + continue; + } + + $content = $subinstruction['content']; + } + } else { + $content = $instruction['content']; + } + + if (!isset($content)) { + throw new \Exception("Cannot find content for {$instruction['name']}"); + } + + $key = "{$service_type}_{$daemon_name}_{$instruction['name']}"; + $titles_by_key[$key] = "{$service->title} : {$daemon->title} : {$instruction['name']}"; + if (!isset($files[$key])) { + $files[$key] = ['from' => '', 'to' => '']; + } + $files[$key][$todo[1]] = $this->filterContent($content); + } + } + } + } + ksort($files); + + $diff_params = ''; + if ($input->hasOption('diff-params') && trim($input->getOption('diff-params')) !== '') { + $diff_params = trim($input->getOption('diff-params')); + } + + // Run diff on each file and output, if anything changed + foreach ($files as $file_key => $content) { + file_put_contents($tmp_from, $content['from']); + file_put_contents($tmp_to, $content['to']); + $diff_output = FileDir::safe_exec("{$check_diff_installed[0]} {$diff_params} {$tmp_from} {$tmp_to}"); + + if (count($diff_output) === 0) { + continue; + } + + $output->writeln('# ' . $titles_by_key[$file_key] . ''); + $output->writeln(implode("\n", $diff_output) . "\n"); + unset($diff_output); + } + + // Remove tmp files again + unlink($tmp_from); + unlink($tmp_to); + + return self::SUCCESS; + } + + private function filterContent(string $content): string + { + $new_content = ''; + + foreach (explode("\n", $content) as $n) { + $n = trim($n); + if (!$n) { + continue; + } + + if (str_starts_with($n, '#')) { + continue; + } + + $new_content .= $n . "\n"; + } + + return $new_content; + } +}