mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(mcp): add REST-backed resource management tools (#12163)
This commit is contained in:
@@ -235,7 +235,7 @@ function restoreScriptResource(string $engine): object
|
||||
/**
|
||||
* @return array{exit: int, stdout: string, stderr: string, calls: list<array{name: string, args: string, header: string, stdin: string}>, dir: string, backup: string, leftovers: list<string>}
|
||||
*/
|
||||
function restoreScriptRun(string $engine, string $contents, bool $dumpAll = false, bool $replaceExisting = false, bool $keepOwners = false, bool $restoreMysqlUsers = false): array
|
||||
function restoreScriptRun(string $engine, string $contents, bool $dumpAll = false, bool $replaceExisting = false, bool $keepOwners = false, bool $restoreMysqlUsers = false, array $env = []): array
|
||||
{
|
||||
$dir = restoreScriptTempDir();
|
||||
|
||||
@@ -257,6 +257,11 @@ if [ ! -t 0 ]; then
|
||||
header=\$(head -c 5 {$escapedStdin}/\$call)
|
||||
fi
|
||||
printf '%s|%s|%s\\n' '{$client}' "\$*" "\$header" >> {$escapedLog}
|
||||
if [ -n "\${STUB_FAIL_MATCH:-}" ]; then
|
||||
case "\$*" in *"\$STUB_FAIL_MATCH"*) exit 1 ;; esac
|
||||
fi
|
||||
case "\$*" in *"SELECT datname FROM pg_database"*) printf '%s\\n' "\${STUB_DATABASES:-}" ;; esac
|
||||
case "\$*" in *rolsuper*) printf '%s\\n' "\${STUB_ROLSUPER-t}" ;; esac
|
||||
exit 0
|
||||
SH);
|
||||
chmod($dir.'/bin/'.$client, 0755);
|
||||
@@ -267,7 +272,7 @@ SH);
|
||||
|
||||
$script = (new DatabaseImportCommandBuilder)->buildRestoreCommand(restoreScriptResource($engine), $backup, $dumpAll, $replaceExisting, $keepOwners, restoreMysqlUsers: $restoreMysqlUsers);
|
||||
|
||||
$result = restoreScriptProcess(['sh', '-c', $script], $dir, [
|
||||
$environment = array_merge([
|
||||
'PATH' => $dir.'/bin'.PATH_SEPARATOR.(getenv('PATH') ?: '/usr/bin:/bin'),
|
||||
'TMPDIR' => $dir,
|
||||
'POSTGRES_USER' => 'postgres',
|
||||
@@ -282,7 +287,8 @@ SH);
|
||||
'MARIADB_ROOT_PASSWORD' => 'root_pass',
|
||||
'MONGO_INITDB_ROOT_USERNAME' => 'root',
|
||||
'MONGO_INITDB_ROOT_PASSWORD' => 'mongo_pass',
|
||||
]);
|
||||
], $env);
|
||||
$result = restoreScriptProcess(['sh', '-c', $script], $dir, array_filter($environment, fn (?string $value): bool => $value !== null));
|
||||
|
||||
$lines = array_values(array_filter(explode("\n", (string) file_get_contents($log)), fn (string $line): bool => $line !== ''));
|
||||
$calls = array_map(function (string $line, int $index) use ($dir): array {
|
||||
@@ -385,6 +391,10 @@ if ($name === 'dropdb') {
|
||||
$database = end($args);
|
||||
if (isset($databases[$database])) { exit(1); }
|
||||
$databases[$database] = 'empty';
|
||||
} elseif (in_array('-Atc', $args, true)) {
|
||||
// Connection and superuser checks run their query as an argument and change nothing.
|
||||
echo str_contains(end($args), 'rolsuper') ? "t\n" : "1\n";
|
||||
exit(0);
|
||||
} else {
|
||||
$variables = [];
|
||||
foreach ($args as $index => $arg) {
|
||||
@@ -496,17 +506,18 @@ test('restores single PostgreSQL archives with pg_restore and SQL with psql', fu
|
||||
expect($run['exit'])->toBe(0, $run['stderr']);
|
||||
restoreScriptExpectCalls($run['calls'], $expectedCalls);
|
||||
})->with([
|
||||
'custom archive' => ['pg-custom', false, [['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'custom archive replacing existing objects' => ['pg-custom', true, [['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl --clean --if-exists -U postgres -d app']]]],
|
||||
'gzip custom archive' => ['pg-custom-gz', false, [['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'gzip custom archive replacing existing objects' => ['pg-custom-gz', true, [['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl --clean --if-exists -U postgres -d app']]]],
|
||||
'tar archive' => ['pg-tar', false, [['pg_restore', 'toc.d', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'gzip tar archive' => ['pg-tar-gz', false, [['pg_restore', 'toc.d', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'plain SQL' => ['pg-sql', false, [['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d app']]]],
|
||||
'gzip SQL' => ['pg-sql-gz', false, [['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d app']]]],
|
||||
'custom archive' => ['pg-custom', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'custom archive replacing existing objects' => ['pg-custom', true, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl --clean --if-exists -U postgres -d app']]]],
|
||||
'gzip custom archive' => ['pg-custom-gz', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'gzip custom archive replacing existing objects' => ['pg-custom-gz', true, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction --no-owner --no-acl --clean --if-exists -U postgres -d app']]]],
|
||||
'tar archive' => ['pg-tar', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'toc.d', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'gzip tar archive' => ['pg-tar-gz', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['pg_restore', 'toc.d', ['--exit-on-error --single-transaction --no-owner --no-acl -U postgres -d app']]]],
|
||||
'plain SQL' => ['pg-sql', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d app']]]],
|
||||
'gzip SQL' => ['pg-sql-gz', false, [['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']], ['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d app']]]],
|
||||
// SQL cannot replace single objects: it restores into a new database, and only a
|
||||
// successful restore replaces the current one, so a failure changes nothing.
|
||||
'plain SQL replacing existing objects' => ['pg-sql', true, [
|
||||
['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']],
|
||||
['psql', 'SELEC', ['-v ON_ERROR_STOP=1 -At', '-v new=coolify_restore_new_', '-v old=coolify_restore_old_']],
|
||||
['createdb', '', ['-U postgres coolify_restore_new_']],
|
||||
['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d coolify_restore_new_']],
|
||||
@@ -514,6 +525,7 @@ test('restores single PostgreSQL archives with pg_restore and SQL with psql', fu
|
||||
['dropdb', '', ['--maintenance-db=template1 -U postgres --if-exists coolify_restore_old_']],
|
||||
]],
|
||||
'gzip SQL replacing existing objects' => ['pg-sql-gz', true, [
|
||||
['psql', '', ['-U postgres -d template1 -Atc', 'SELECT 1']],
|
||||
['psql', 'SELEC', ['-v ON_ERROR_STOP=1 -At', '-v new=coolify_restore_new_', '-v old=coolify_restore_old_']],
|
||||
['createdb', '', ['-U postgres coolify_restore_new_']],
|
||||
['psql', '-- Po', ['-v ON_ERROR_STOP=1 --single-transaction -U postgres -d coolify_restore_new_']],
|
||||
@@ -526,8 +538,8 @@ test('keeps PostgreSQL owners and privileges only when requested', function (str
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture($fixture), keepOwners: true);
|
||||
|
||||
expect($run['exit'])->toBe(0, $run['stderr']);
|
||||
restoreScriptExpectCalls($run['calls'], [['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction -U postgres -d app']]]);
|
||||
expect($run['calls'][0]['args'])->not->toContain('--no-owner')->not->toContain('--no-acl');
|
||||
restoreScriptExpectCalls($run['calls'], [['psql', '', ['SELECT 1']], ['pg_restore', 'PGDMP', ['--exit-on-error --single-transaction -U postgres -d app']]]);
|
||||
expect($run['calls'][1]['args'])->not->toContain('--no-owner')->not->toContain('--no-acl');
|
||||
})->with(['custom archive' => ['pg-custom'], 'gzip custom archive' => ['pg-custom-gz']]);
|
||||
|
||||
test('rejects unsupported single PostgreSQL backups before calling any client', function (string $fixture, string $message) {
|
||||
@@ -541,23 +553,30 @@ test('rejects unsupported single PostgreSQL backups before calling any client',
|
||||
'binary garbage' => ['garbage', 'Unsupported PostgreSQL backup format'],
|
||||
]);
|
||||
|
||||
test('restores PostgreSQL backups containing all databases after checking the format', function (string $fixture, array $restoreCall) {
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture($fixture), dumpAll: true);
|
||||
test('restores PostgreSQL backups containing all databases after checking the format', function (string $fixture, array $restoreCall, array $environment, string $user) {
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture($fixture), dumpAll: true, env: $environment + ['STUB_DATABASES' => 'app']);
|
||||
|
||||
$recreate = [
|
||||
['psql', '', ['-U postgres -c', 'pg_terminate_backend']],
|
||||
['psql', '', ['-U postgres -t -c', 'SELECT datname FROM pg_database']],
|
||||
['createdb', '', ['-U postgres app']],
|
||||
['psql', '', ["-U {$user} -d template1 -Atc", 'rolsuper']],
|
||||
['psql', '', ["-U {$user} -d template1 -c", 'pg_terminate_backend']],
|
||||
['psql', '', ["-U {$user} -d template1 -t -c", 'SELECT datname FROM pg_database']],
|
||||
['dropdb', '', ["-U {$user} --maintenance-db=template1 --if-exists app"]],
|
||||
['createdb', '', ["-U {$user} app"]],
|
||||
];
|
||||
$inspect = $restoreCall[0] === 'pg_restore' ? [['pg_restore', 'PGDMP', ['-l']]] : [];
|
||||
|
||||
expect($run['exit'])->toBe(0, $run['stderr']);
|
||||
$restoreCall[2] = ["-U {$user} -d app"];
|
||||
restoreScriptExpectCalls($run['calls'], [...$inspect, ...$recreate, $restoreCall]);
|
||||
})->with([
|
||||
'custom archive' => ['pg-custom', ['pg_restore', 'PGDMP', ['-U postgres -d app']]],
|
||||
'gzip custom archive' => ['pg-custom-gz', ['pg_restore', 'PGDMP', ['-U postgres -d app']]],
|
||||
'plain SQL' => ['pg-sql', ['psql', '-- Po', ['-U postgres -d app']]],
|
||||
'gzip SQL' => ['pg-sql-gz', ['psql', '-- Po', ['-U postgres -d app']]],
|
||||
])->with([
|
||||
'POSTGRES_USER' => [[], 'postgres'],
|
||||
'PGUSER' => [['POSTGRES_USER' => null, 'PGUSER' => 'custom_user'], 'custom_user'],
|
||||
'POSTGRESQL_USERNAME' => [['POSTGRES_USER' => null, 'POSTGRESQL_USERNAME' => 'bitnami'], 'bitnami'],
|
||||
]);
|
||||
|
||||
test('rejects unsupported PostgreSQL backups containing all databases before dropping anything', function () {
|
||||
@@ -568,6 +587,46 @@ test('rejects unsupported PostgreSQL backups containing all databases before dro
|
||||
->and($run['calls'])->toBe([]);
|
||||
});
|
||||
|
||||
test('finds the PostgreSQL user from POSTGRES_USER, PGUSER, or POSTGRESQL_USERNAME', function (array $env, string $user) {
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture('pg-custom'), env: ['POSTGRES_USER' => null, 'POSTGRES_DB' => null, ...$env]);
|
||||
|
||||
expect($run['exit'])->toBe(0, $run['stderr']);
|
||||
restoreScriptExpectCalls($run['calls'], [
|
||||
['psql', '', ["-U {$user} -d template1 -Atc"]],
|
||||
['pg_restore', 'PGDMP', ["-U {$user} -d {$user}"]],
|
||||
]);
|
||||
})->with([
|
||||
'POSTGRES_USER' => [['POSTGRES_USER' => 'app'], 'app'],
|
||||
'PGUSER' => [['PGUSER' => 'pguser'], 'pguser'],
|
||||
'Bitnami POSTGRESQL_USERNAME' => [['POSTGRESQL_USERNAME' => 'bitnami'], 'bitnami'],
|
||||
'POSTGRES_USER wins over the others' => [['POSTGRES_USER' => 'app', 'PGUSER' => 'pguser', 'POSTGRESQL_USERNAME' => 'bitnami'], 'app'],
|
||||
'nothing set' => [[], 'postgres'],
|
||||
]);
|
||||
|
||||
test('stops a PostgreSQL restore before any change when it cannot connect as the user', function (string $fixture, bool $dumpAll, bool $replaceExisting, string $message) {
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture($fixture), $dumpAll, $replaceExisting, env: ['POSTGRES_USER' => null, 'STUB_FAIL_MATCH' => '-d template1 -Atc']);
|
||||
|
||||
$changing = array_filter($run['calls'], fn (array $call): bool => ! str_contains($call['args'], '-d template1 -Atc') && ! ($call['name'] === 'pg_restore' && $call['args'] === '-l'));
|
||||
|
||||
expect($run['exit'])->toBe(1)
|
||||
->and($run['stderr'])->toContain($message)->toContain('postgres')->toContain('Nothing was changed.')
|
||||
->and($changing)->toBe([]);
|
||||
})->with([
|
||||
'custom archive' => ['pg-custom', false, false, 'Cannot connect to PostgreSQL as user'],
|
||||
'SQL' => ['pg-sql', false, false, 'Cannot connect to PostgreSQL as user'],
|
||||
'SQL replacing existing objects' => ['pg-sql', false, true, 'Cannot connect to PostgreSQL as user'],
|
||||
'all databases' => ['pg-custom', true, false, 'needs a PostgreSQL superuser'],
|
||||
]);
|
||||
|
||||
test('refuses to restore all PostgreSQL databases as a user that is not a superuser', function () {
|
||||
$run = restoreScriptRun('postgresql', restoreScriptFixture('pg-sql'), dumpAll: true, env: ['POSTGRES_USER' => null, 'POSTGRESQL_USERNAME' => 'bitnami', 'STUB_ROLSUPER' => 'f']);
|
||||
|
||||
expect($run['exit'])->toBe(1)
|
||||
->and($run['stderr'])->toContain('needs a PostgreSQL superuser')->toContain('bitnami')->toContain('Nothing was changed.')
|
||||
->and(array_column($run['calls'], 'name'))->toBe(['psql'])
|
||||
->and($run['calls'][0]['args'])->toContain('rolsuper');
|
||||
});
|
||||
|
||||
test('restores single MySQL and MariaDB SQL backups', function (string $engine, string $fixture) {
|
||||
$run = restoreScriptRun($engine, restoreScriptFixture($fixture));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user