mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 16:46:37 +00:00
An all-databases MySQL/MariaDB restore also restored the mysql system database, so after the next restart the root and app passwords were the source ones and the MariaDB health check failed. The restore now skips mysql, sys, performance_schema and information_schema by default. The new option "Restore users and privileges" (restore_mysql_users in the API) keeps the old behaviour.
199 lines
9.4 KiB
PHP
199 lines
9.4 KiB
PHP
<?php
|
|
|
|
use App\Models\ServiceDatabase;
|
|
use App\Models\StandaloneMariadb;
|
|
use App\Models\StandaloneMongodb;
|
|
use App\Models\StandaloneMysql;
|
|
use App\Models\StandalonePostgresql;
|
|
use App\Models\StandaloneRedis;
|
|
use App\Models\StandaloneSqlite;
|
|
use App\Support\DatabaseImport\DatabaseImportCommandBuilder;
|
|
|
|
function importResource(string $class, ?string $databaseType = null): object
|
|
{
|
|
$resource = Mockery::mock($class);
|
|
$resource->shouldReceive('getMorphClass')->andReturn($class);
|
|
if ($class === ServiceDatabase::class) {
|
|
$resource->shouldReceive('databaseType')->andReturn($databaseType);
|
|
}
|
|
if ($class === StandaloneSqlite::class) {
|
|
$resource->shouldReceive('databaseFilePath')->andReturn('/var/lib/sqlite/database.sqlite');
|
|
}
|
|
|
|
return $resource;
|
|
}
|
|
|
|
test('builds database-specific restore commands', function (string $class, ?string $type, string $needle) {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
|
|
$command = $builder->buildRestoreCommand(importResource($class, $type), '/tmp/restore file', false);
|
|
|
|
expect($command)->toContain($needle)->toContain("'/tmp/restore file'");
|
|
})->with([
|
|
'postgresql' => [StandalonePostgresql::class, null, 'pg_restore'],
|
|
'mysql' => [StandaloneMysql::class, null, 'mysql -u $MYSQL_USER'],
|
|
'mariadb' => [StandaloneMariadb::class, null, 'mariadb -u $MARIADB_USER'],
|
|
'mongodb' => [StandaloneMongodb::class, null, 'mongorestore'],
|
|
'sqlite' => [StandaloneSqlite::class, null, '.restore'],
|
|
'service postgres' => [ServiceDatabase::class, 'postgresql', 'pg_restore'],
|
|
'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql -u $MYSQL_USER'],
|
|
'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb -u $MARIADB_USER'],
|
|
'service mongo' => [ServiceDatabase::class, 'mongodb', 'mongorestore'],
|
|
]);
|
|
|
|
test('decompresses gzip backups for single-database mysql and mariadb restores', function (string $class, ?string $type, string $client) {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
|
|
$command = $builder->buildRestoreCommand(importResource($class, $type), '/tmp/restore file.sql.gz', false);
|
|
|
|
expect($command)
|
|
->toStartWith("backup='/tmp/restore file.sql.gz'\n")
|
|
->toContain('is_gzip() { [ "$(head -c 2 "$backup" | od -An -tx1 | tr -d \' \n\')" = 1f8b ]; }')
|
|
->toEndWith("stream | {$client}");
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, null, 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
|
|
'mariadb' => [StandaloneMariadb::class, null, 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'],
|
|
'service mysql' => [ServiceDatabase::class, 'mysql', 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'],
|
|
'service mariadb' => [ServiceDatabase::class, 'mariadb', 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE'],
|
|
]);
|
|
|
|
test('builds dump-all commands and postgres safety scan', function () {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
$postgres = importResource(StandalonePostgresql::class);
|
|
|
|
$safety = $builder->buildPostgresSafetyCommand($postgres, 'postgres-safe', '/tmp/dump.sql.gz');
|
|
$script = $builder->buildPostgresRestoreScanScript($postgres, '/tmp/dump.sql.gz');
|
|
|
|
expect($builder->buildRestoreCommand($postgres, '/tmp/dump.sql.gz', true))
|
|
->toContain('pg_terminate_backend')
|
|
->toStartWith("backup='/tmp/dump.sql.gz'\n")
|
|
->and($safety)
|
|
->toContain('COPY ... PROGRAM')
|
|
->toContain('docker exec postgres-safe')
|
|
->toContain('pg_restore -l')
|
|
->toContain('pg_restore -f -')
|
|
->toContain('unable to inspect custom archive')
|
|
->not->toContain('then exit 0')
|
|
->and($script)
|
|
->toContain("tr '\\n\\r\\t'");
|
|
});
|
|
|
|
test('restores dump-all PostgreSQL custom archives with pg_restore from the decompressed stream', function () {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump.gz', true);
|
|
|
|
// pg_restore cannot read gzip files, so it must receive the decompressed archive on stdin.
|
|
expect($command)
|
|
->toContain('stream | pg_restore -U ${POSTGRES_USER} -d "$db"')
|
|
->toContain('stream | psql -U ${POSTGRES_USER} -d "$db"')
|
|
->not->toContain("-d \"\$db\" '/tmp/backup.dump.gz'");
|
|
});
|
|
|
|
test('postgres safety command is null for non-postgres databases', function () {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
|
|
expect($builder->buildPostgresSafetyCommand(importResource(StandaloneMysql::class), 'mysql-test', '/tmp/restore_test'))
|
|
->toBeNull();
|
|
});
|
|
|
|
test('dump-all mysql and mariadb commands use valid shell parameter expansions', function (string $class, string $binary, string $prefix) {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
|
|
$command = $builder->buildRestoreCommand(importResource($class), '/tmp/dump.sql.gz', true);
|
|
|
|
$rootPassword = '${'.$prefix.'_ROOT_PASSWORD}';
|
|
$database = '${'.$prefix.'_DATABASE:-default}';
|
|
|
|
expect($command)
|
|
->toContain($binary)
|
|
->toStartWith("backup='/tmp/dump.sql.gz'\n")
|
|
->toContain('-p'.$rootPassword)
|
|
->toContain('CREATE DATABASE IF NOT EXISTS \`'.$database.'\`')
|
|
->and(substr_count($command, $rootPassword))->toBe(6)
|
|
->and(substr_count($command, $database))->toBe(2)
|
|
->and($command)->not->toContain('${{');
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, 'mysql', 'MYSQL'],
|
|
'mariadb' => [StandaloneMariadb::class, 'mariadb', 'MARIADB'],
|
|
]);
|
|
|
|
test('stops PostgreSQL restores on the first error without replacing existing objects by default', function () {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
$postgres = importResource(StandalonePostgresql::class);
|
|
|
|
expect($builder->buildRestoreCommand($postgres, '/tmp/backup.dump', false, false))
|
|
->toContain('--exit-on-error')
|
|
->not->toContain('--clean')
|
|
->not->toContain('--if-exists');
|
|
});
|
|
|
|
test('replaces existing PostgreSQL objects when requested', function () {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
$postgres = importResource(StandalonePostgresql::class);
|
|
|
|
expect($builder->buildRestoreCommand($postgres, '/tmp/backup.dump', false, true))
|
|
->toContain('--clean')
|
|
->toContain('--if-exists')
|
|
->toContain('--exit-on-error');
|
|
});
|
|
|
|
test('restores PostgreSQL archives without owners and privileges by default', function (bool $replaceExisting) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump', false, $replaceExisting);
|
|
|
|
// Dumps from other hosts (for example RDS) reference roles that do not exist here, and
|
|
// --single-transaction would roll back the whole restore on the first ALTER OWNER.
|
|
expect($command)->toContain('pg_restore --exit-on-error --single-transaction --no-owner --no-acl');
|
|
})->with(['keep existing objects' => [false], 'replace existing objects' => [true]]);
|
|
|
|
test('keeps PostgreSQL owners and privileges when requested', function () {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandalonePostgresql::class), '/tmp/backup.dump', false, false, keepOwners: true);
|
|
|
|
expect($command)->toContain('pg_restore --exit-on-error --single-transaction -U')
|
|
->not->toContain('--no-owner')
|
|
->not->toContain('--no-acl');
|
|
});
|
|
|
|
test('skips the system databases of MySQL and MariaDB all-databases backups by default', function (string $class, string $binary) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource($class), '/tmp/backup.sql', true);
|
|
|
|
expect($command)->toContain('command -v awk')
|
|
->toMatch('/stream \| \{?\s*awk /')
|
|
->toContain('"mysql"')->toContain('"sys"')->toContain('"performance_schema"')->toContain('"information_schema"')
|
|
->toContain("| {$binary} -u root");
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, 'mysql'],
|
|
'mariadb' => [StandaloneMariadb::class, 'mariadb'],
|
|
]);
|
|
|
|
test('restores the system databases of MySQL and MariaDB all-databases backups when requested', function (string $class, string $binary) {
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource($class), '/tmp/backup.sql', true, restoreMysqlUsers: true);
|
|
|
|
expect($command)->not->toContain('awk')
|
|
->toContain("stream | {$binary} -u root");
|
|
})->with([
|
|
'mysql' => [StandaloneMysql::class, 'mysql'],
|
|
'mariadb' => [StandaloneMariadb::class, 'mariadb'],
|
|
]);
|
|
|
|
test('restores single MySQL databases without the system database filter', function () {
|
|
expect((new DatabaseImportCommandBuilder)->buildRestoreCommand(importResource(StandaloneMysql::class), '/tmp/backup.sql', false))
|
|
->not->toContain('awk');
|
|
});
|
|
|
|
test('restores SQLite backups into the selected database file', function () {
|
|
$resource = Mockery::mock(StandaloneSqlite::class);
|
|
$resource->shouldReceive('getMorphClass')->andReturn(StandaloneSqlite::class);
|
|
$resource->shouldReceive('databaseFilePath')->once()->with('cache.db')->andReturn('/var/lib/sqlite/cache.db');
|
|
|
|
$command = (new DatabaseImportCommandBuilder)->buildRestoreCommand($resource, '/tmp/backup', false, sqliteDatabase: 'cache.db');
|
|
|
|
expect($command)->toContain("sqlite3 -bail '/var/lib/sqlite/cache.db'");
|
|
});
|
|
|
|
test('rejects unsupported database types', function () {
|
|
$builder = new DatabaseImportCommandBuilder;
|
|
$redis = importResource(StandaloneRedis::class);
|
|
|
|
expect(fn () => $builder->buildRestoreCommand($redis, '/tmp/backup', false))
|
|
->toThrow(InvalidArgumentException::class, 'not supported');
|
|
});
|