Merge remote-tracking branch 'origin/main' into next

This commit is contained in:
github-actions[bot]
2026-10-05 13:33:55 +00:00
9 changed files with 103 additions and 9 deletions
@@ -433,7 +433,7 @@ class ApplicationsController extends Controller
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
@@ -632,7 +632,7 @@ class ApplicationsController extends Controller
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
@@ -831,7 +831,7 @@ class ApplicationsController extends Controller
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
'is_preserve_repository_enabled' => ['type' => 'boolean', 'default' => false, 'description' => 'Preserve repository during deployment.'],
@@ -1001,7 +1001,7 @@ class ApplicationsController extends Controller
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
],
@@ -1167,7 +1167,7 @@ class ApplicationsController extends Controller
'http_basic_auth_argon2id_time_cost' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 12, 'default' => 4, 'description' => 'Argon2id time cost (iterations) for the HTTP Basic Authentication password.'],
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
'force_domain_override' => ['type' => 'boolean', 'description' => 'Force domain usage even if conflicts are detected. Default is false.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Default: true.'],
'autogenerate_domain' => ['type' => 'boolean', 'default' => true, 'description' => 'If true and domains is empty, auto-generate a domain using the server\'s wildcard domain or sslip.io fallback. Ignored for the dockercompose build pack, which uses docker_compose_domains. Default: true.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. So if you write $ in the labels, it will be saved as $$. If you want to use env variables inside the labels, turn this off.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the application.'],
],
@@ -1319,7 +1319,8 @@ class ApplicationsController extends Controller
$fqdn = ValidationPatterns::normalizeApplicationDomains($request->domains);
$request->offsetSet('domains', $fqdn);
}
$autogenerateDomain = $request->boolean('autogenerate_domain', true);
// A Docker Compose application uses per-service domains; a top-level domain is never routed.
$autogenerateDomain = $request->boolean('autogenerate_domain', true) && $request->build_pack !== 'dockercompose';
$instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
@@ -3364,6 +3364,10 @@ class DatabasesController extends Controller
response: 409,
description: 'Another start, restart or import of this database is already in progress.',
),
new OA\Response(
response: 422,
description: 'The database cannot start yet, for example because a CA certificate is missing.',
),
]
)]
public function action_deploy(Request $request)
@@ -3386,6 +3390,10 @@ class DatabasesController extends Controller
if (str($database->status)->contains('running')) {
return response()->json(['message' => 'Database is already running.'], 400);
}
$prerequisiteError = StartDatabase::prerequisiteError($database);
if ($prerequisiteError !== null) {
return response()->json(['message' => $prerequisiteError], 422);
}
$reservation = StartDatabase::reserveOperation($database);
if ($reservation === null) {
return response()->json(['message' => ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE], 409);
@@ -3558,6 +3566,10 @@ class DatabasesController extends Controller
response: 409,
description: 'Another start, restart or import of this database is already in progress.',
),
new OA\Response(
response: 422,
description: 'The database cannot start yet, for example because a CA certificate is missing.',
),
]
)]
public function action_restart(Request $request)
@@ -3577,6 +3589,10 @@ class DatabasesController extends Controller
$this->authorize('manage', $database);
$prerequisiteError = StartDatabase::prerequisiteError($database);
if ($prerequisiteError !== null) {
return response()->json(['message' => $prerequisiteError], 422);
}
$reservation = StartDatabase::reserveOperation($database);
if ($reservation === null) {
return response()->json(['message' => ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE], 409);
@@ -559,6 +559,11 @@ class DeployController extends Controller
} catch (AuthorizationException $e) {
return ['message' => 'Unauthorized to start this database.', 'deployment_uuid' => null];
}
$prerequisiteError = StartDatabase::prerequisiteError($resource);
if ($prerequisiteError !== null) {
$message = $prerequisiteError;
break;
}
$reservation = StartDatabase::reserveOperation($resource);
if ($reservation === null) {
$message = ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE;
@@ -158,6 +158,7 @@ class Heading extends Component
return;
}
$this->auditDatabaseAction('ui.database.restarted');
$this->dispatch('info', 'Restarting database.');
$this->markDeploymentRunning($activity);
$this->js("window.dispatchEvent(new CustomEvent('startdatabase'))");
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
+5
View File
@@ -141,6 +141,11 @@ class Control extends Tool
return ['message' => 'Database is already running.'];
}
$prerequisiteError = StartDatabase::prerequisiteError($database);
if ($prerequisiteError !== null) {
throw new \RuntimeException($prerequisiteError);
}
$reservation = StartDatabase::reserveOperation($database);
if ($reservation === null) {
throw new \RuntimeException(ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE);
@@ -183,7 +183,7 @@
window.dispatchEvent(new CustomEvent('database-action-finished'));
return;
}
window.dispatchEvent(new CustomEvent('startdatabase'));
// start() opens the log dialog only when the start was queued.
try {
await $wire.$call('start');
} finally {
@@ -197,8 +197,7 @@
return;
}
window.dispatchEvent(new CustomEvent('database-busy'));
$wire.$dispatch('info', 'Restarting database.');
window.dispatchEvent(new CustomEvent('startdatabase'));
// restart() opens the log dialog only when the restart was queued.
try {
await $wire.$call('restart');
} finally {
@@ -266,6 +266,26 @@ test('API create accepts a safe docker_compose_raw', function () {
->assertCreated();
});
test('API create does not generate a domain for a docker compose application', function () {
Queue::fake();
$response = $this->withHeaders(['Authorization' => 'Bearer '.$this->token])
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/compose-app',
'git_branch' => 'main',
'build_pack' => 'dockercompose',
'ports_exposes' => '80',
'autogenerate_domain' => true,
'docker_compose_raw' => SAFE_APPLICATION_COMPOSE,
])
->assertCreated();
expect(Application::query()->where('uuid', $response->json('uuid'))->value('fqdn'))->toBeNull();
});
test('API update does not accept docker_compose_raw', function () {
$this->withHeaders(['Authorization' => 'Bearer '.$this->token])
->patchJson("/api/v1/applications/{$this->application->uuid}", [
@@ -86,3 +86,9 @@ test('public github repositories are stored as owner and repository with the pub
->and($application->source_type)->toBe(GithubApp::class)
->and($application->source_id)->toBe(0);
});
test('a public application gets a generated domain by default', function () {
$application = createPublicGitApplication(['autogenerate_domain' => true]);
expect($application->fqdn)->not->toBeEmpty();
});
@@ -19,6 +19,7 @@ use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Spatie\Activitylog\Models\Activity;
use Symfony\Component\Yaml\Yaml;
@@ -174,3 +175,43 @@ it('reads the data mount with sudo on a non-root server', function () {
expect($database->fresh()->anonymousDataVolume())->toBe('3f1c0ffee')
->and(ranRemoteCommandContaining("sudo docker inspect --format '{{range .Mounts}}{{if eq .Destination \"/var/lib/clickhouse\"}}{{.Type}} {{.Name}}{{end}}{{end}}' '{$database->uuid}' 2>/dev/null || sudo true"))->toBeTrue();
});
it('rejects an API start or restart of a blocked database at once', function (string $method, string $path) {
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
fakeClickhouseDataMount('3f1c0ffee');
$plainToken = Str::random(40);
$token = $this->owner->tokens()->create([
'name' => 'clickhouse-test',
'token' => hash('sha256', $plainToken),
'abilities' => ['*'],
'team_id' => $this->team->id,
]);
$response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken)
->json($method, str_replace('{uuid}', $database->uuid, $path));
$response->assertStatus(422);
expect($response->json('message'))->toContain('unnamed Docker volume 3f1c0ffee');
Queue::assertNothingPushed();
})->with([
'start' => ['POST', '/api/v1/databases/{uuid}/start'],
'restart' => ['POST', '/api/v1/databases/{uuid}/restart'],
]);
it('reports a blocked database in the deploy API', function () {
$database = create_standalone_clickhouse($this->environment->id, $this->destination);
fakeClickhouseDataMount('3f1c0ffee');
$plainToken = Str::random(40);
$token = $this->owner->tokens()->create([
'name' => 'clickhouse-test',
'token' => hash('sha256', $plainToken),
'abilities' => ['*'],
'team_id' => $this->team->id,
]);
$response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken)
->postJson('/api/v1/deploy', ['uuid' => $database->uuid]);
expect($response->json('deployments.0.message'))->toContain('unnamed Docker volume 3f1c0ffee');
Queue::assertNothingPushed();
});