Add a "azuracast:setup:rollback" CLI command to roll back to the DB migration associated with a given stable release.

This commit is contained in:
Buster Neece 2023-09-03 12:10:40 -05:00
parent c205487620
commit 7706457322
No known key found for this signature in database
16 changed files with 272 additions and 60 deletions

View File

@ -27,6 +27,7 @@ return function (App\Event\BuildConsoleCommands $event) {
'azuracast:config:migrate' => Command\MigrateConfigCommand::class,
'azuracast:setup:migrate' => Command\MigrateDbCommand::class,
'azuracast:setup:fixtures' => Command\SetupFixturesCommand::class,
'azuracast:setup:rollback' => Command\RollbackDbCommand::class,
'azuracast:setup' => Command\SetupCommand::class,
'azuracast:radio:restart' => Command\RestartRadioCommand::class,
'azuracast:sync:nowplaying' => Command\Sync\NowPlayingCommand::class,

View File

@ -7,8 +7,11 @@ namespace App\Console\Command;
use App\Console\Command\Traits\PassThruProcess;
use App\Container\EntityManagerAwareTrait;
use App\Container\EnvironmentAwareTrait;
use App\Entity\StorageLocation;
use Exception;
use RuntimeException;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
abstract class AbstractDatabaseCommand extends CommandAbstract
{
@ -91,4 +94,53 @@ abstract class AbstractDatabaseCommand extends CommandAbstract
$commandEnvVars
);
}
protected function saveOrRestoreDatabase(
SymfonyStyle $io,
): string {
$io->section(__('Backing up initial database state...'));
$tempDir = StorageLocation::DEFAULT_BACKUPS_PATH;
$dbDumpPath = $tempDir . '/pre_migration_db.sql';
$fs = new Filesystem();
if ($fs->exists($dbDumpPath)) {
$io->info([
__('We detected a database restore file from a previous (possibly failed) migration.'),
__('Attempting to restore that now...'),
]);
$this->restoreDatabaseDump($io, $dbDumpPath);
} else {
$this->dumpDatabase($io, $dbDumpPath);
}
return $dbDumpPath;
}
protected function tryEmergencyRestore(
SymfonyStyle $io,
string $dbDumpPath
): int {
$io->section(__('Attempting to roll back to previous database state...'));
try {
$this->restoreDatabaseDump($io, $dbDumpPath);
$io->warning([
__('Your database was restored due to a failed migration.'),
__('Please report this bug to our developers.'),
]);
return 0;
} catch (Exception $e) {
$io->error(
sprintf(
__('Restore failed: %s'),
$e->getMessage()
)
);
return 1;
}
}
}

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Console\Command;
use App\Entity\StorageLocation;
use Exception;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
@ -41,42 +40,11 @@ final class MigrateDbCommand extends AbstractDatabaseCommand
}
// Back up current DB state.
$io->section(__('Backing up initial database state...'));
$tempDir = StorageLocation::DEFAULT_BACKUPS_PATH;
$dbDumpPath = $tempDir . '/pre_migration_db.sql';
$fs = new Filesystem();
if ($fs->exists($dbDumpPath)) {
$io->info([
__('We detected a database restore file from a previous (possibly failed) migration.'),
__('Attempting to restore that now...'),
]);
try {
$this->restoreDatabaseDump($io, $dbDumpPath);
} catch (Exception $e) {
$io->error(
sprintf(
__('Restore failed: %s'),
$e->getMessage()
)
);
return 1;
}
} else {
try {
$this->dumpDatabase($io, $dbDumpPath);
} catch (Exception $e) {
$io->error(
sprintf(
__('Initial backup failed: %s'),
$e->getMessage()
)
);
return 1;
}
try {
$dbDumpPath = $this->saveOrRestoreDatabase($io);
} catch (Exception $e) {
$io->error($e->getMessage());
return 1;
}
// Attempt DB migration.
@ -99,27 +67,9 @@ final class MigrateDbCommand extends AbstractDatabaseCommand
)
);
$io->section(__('Attempting to roll back to previous database state...'));
try {
$this->restoreDatabaseDump($io, $dbDumpPath);
$io->warning([
__('Your database was restored due to a failed migration.'),
__('Please report this bug to our developers.'),
]);
return 0;
} catch (Exception $e) {
$io->error(
sprintf(
__('Restore failed: %s'),
$e->getMessage()
)
);
return 1;
}
return $this->tryEmergencyRestore($io, $dbDumpPath);
} finally {
$fs->remove($dbDumpPath);
(new Filesystem())->remove($dbDumpPath);
}
$io->newLine();

View File

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
namespace App\Console\Command;
use App\Container\ContainerAwareTrait;
use App\Container\EnvironmentAwareTrait;
use App\Entity\Attributes\StableMigration;
use Exception;
use FilesystemIterator;
use InvalidArgumentException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use ReflectionClass;
use SplFileInfo;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Throwable;
#[AsCommand(
name: 'azuracast:setup:rollback',
description: 'Roll back the database to the state associated with a certain stable release.',
)]
final class RollbackDbCommand extends AbstractDatabaseCommand
{
use ContainerAwareTrait;
use EnvironmentAwareTrait;
protected function configure(): void
{
$this->addArgument('version', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title(__('Roll Back Database'));
// Pull migration corresponding to the stable version specified.
try {
$version = $input->getArgument('version');
$migrationVersion = $this->findMigration($version);
} catch (Throwable $e) {
$io->error($e->getMessage());
return 1;
}
$this->runCommand(
$output,
'migrations:sync-metadata-storage'
);
// Attempt DB migration.
$io->section(__('Running database migrations...'));
// Back up current DB state.
try {
$dbDumpPath = $this->saveOrRestoreDatabase($io);
} catch (Exception $e) {
$io->error($e->getMessage());
return 1;
}
try {
$io->info($migrationVersion);
$this->runCommand(
$output,
'migrations:migrate',
[
'--allow-no-migration' => true,
'version' => $migrationVersion,
]
);
} catch (Exception $e) {
// Rollback to the DB dump from earlier.
$io->error(
sprintf(
__('Database migration failed: %s'),
$e->getMessage()
)
);
return $this->tryEmergencyRestore($io, $dbDumpPath);
} finally {
(new Filesystem())->remove($dbDumpPath);
}
$io->newLine();
$io->success(
sprintf(
__('Database rolled back to stable release version "%s".'),
$version
)
);
return 0;
}
protected function findMigration(string $version): string
{
$version = trim($version);
if (empty($version)) {
throw new InvalidArgumentException('No version specified.');
}
$versionParts = explode('.', $version);
if (3 !== count($versionParts)) {
throw new InvalidArgumentException(
'Invalid version specified. Version must be in the form of x.x.x, i.e. 0.19.0.'
);
}
$migrationsDir = $this->environment->getBaseDirectory() . '/src/Entity/Migration';
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($migrationsDir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
$migrationFiles = [];
/** @var SplFileInfo $file */
foreach ($iterator as $file) {
// Skip dotfiles
$fileName = $file->getBasename('.php');
if ($fileName == $file->getBasename()) {
continue;
}
$className = 'App\\Entity\\Migration\\' . $fileName;
$migrationFiles[$fileName] = $className;
}
$migrationFiles = array_reverse($migrationFiles);
/** @var class-string $migrationClassName */
foreach ($migrationFiles as $migrationClassName) {
$reflClass = new ReflectionClass($migrationClassName);
$reflAttrs = $reflClass->getAttributes(StableMigration::class);
foreach ($reflAttrs as $reflAttrInfo) {
/** @var StableMigration $reflAttr */
$reflAttr = $reflAttrInfo->newInstance();
if ($version === $reflAttr->version) {
return $migrationClassName;
}
}
}
throw new InvalidArgumentException(
'No migration found for the specified version. Make sure to specify a version after 0.17.0.'
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Entity\Attributes;
use Attribute;
/**
* Mark a database migration as the last migration before a stable version was tagged.
*/
#[Attribute(Attribute::TARGET_CLASS | ATTRIBUTE::IS_REPEATABLE)]
final class StableMigration
{
public function __construct(
public string $version
) {
}
}

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.0')]
final class Version20220605052847 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.1')]
final class Version20220611123923 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.2')]
final class Version20220626171758 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.3')]
final class Version20220724223136 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.4')]
final class Version20221008043751 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.5')]
final class Version20221102125558 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[StableMigration('0.17.6')]
final class Version20221110212745 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,12 +4,11 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
#[StableMigration('0.17.7')]
final class Version20230102192652 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,14 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[
StableMigration('0.18.1'),
StableMigration('0.18.0')
]
final class Version20230410210554 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,10 +4,16 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Exception;
#[
StableMigration('0.18.5'),
StableMigration('0.18.3'),
StableMigration('0.18.2')
]
final class Version20230602095822 extends AbstractMigration
{
public function getDescription(): string

View File

@ -4,9 +4,14 @@ declare(strict_types=1);
namespace App\Entity\Migration;
use App\Entity\Attributes\StableMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
#[
StableMigration('0.19.1'),
StableMigration('0.19.0')
]
final class Version20230803181406 extends AbstractMigration
{
public function getDescription(): string