Merge pull request #2881 from coollabsio/next

v4.0.0-beta.318
This commit is contained in:
Andras Bacsai 2024-07-24 14:26:39 +02:00 committed by GitHub
commit 6cc86a3c82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1290 additions and 380 deletions

View File

@ -48,6 +48,7 @@ # Donations
<a href="https://trieve.ai/?ref=coolify.io" target="_blank"><img src="./other/logos/trieve_bg.png" alt="trieve logo" width="180"/></a>
<a href="https://blacksmith.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/blacksmith.svg" alt="blacksmith logo" width="200"/></a>
<a href="https://latitude.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/latitude.svg" alt="latitude logo" width="200"/></a>
<a href="https://brand.dev/?ref=coolify.io" target="_blank"><img src="./other/logos/branddev.png" alt="branddev logo" width="200"/></a>
## Github Sponsors ($40+)
<a href="https://serpapi.com/?ref=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>

View File

@ -9,7 +9,7 @@ class StopApplication
{
use AsAction;
public function handle(Application $application)
public function handle(Application $application, bool $previewDeployments = false)
{
if ($application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
@ -26,7 +26,12 @@ public function handle(Application $application)
if (! $server->isFunctional()) {
return 'Server is not functional';
}
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($previewDeployments) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
} else {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
}
ray($containers);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');

View File

@ -21,7 +21,6 @@ public function handle(Server $server, bool $reset = false)
"cat $proxy_path/docker-compose.yml",
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
}

View File

@ -0,0 +1,67 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidateServer
{
use AsAction;
public ?string $uptime = null;
public ?string $error = null;
public ?string $supported_os_type = null;
public ?string $docker_installed = null;
public ?string $docker_compose_installed = null;
public ?string $docker_version = null;
public function handle(Server $server)
{
$server->update([
'validation_logs' => null,
]);
['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection();
if (! $this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->supported_os_type = $server->validateOS();
if (! $this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_installed = $server->validateDockerEngine();
$this->docker_compose_installed = $server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_version = $server->validateDockerEngineVersion();
if ($this->docker_version) {
return 'OK';
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
}
}

View File

@ -18,7 +18,7 @@ public function handle()
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
// send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([
'ip' => '1.2.3.4',
]);

View File

@ -135,8 +135,14 @@ public function environment_details(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first();
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
if (! $request->environment_name) {
return response()->json(['message' => 'Environment name is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
$environment = $project->environments()->whereName($request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
@ -144,4 +150,276 @@ public function environment_details(Request $request)
return response()->json(serializeApiResponse($environment));
}
#[OA\Post(
summary: 'Create',
description: 'Create Project.',
path: '/projects',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
requestBody: new OA\RequestBody(
required: true,
description: 'Project created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Project created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the project.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function create_project(Request $request)
{
$allowedFields = ['name', 'description'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|required',
'description' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$project = Project::create([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,
]);
return response()->json([
'uuid' => $project->uuid,
])->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
requestBody: new OA\RequestBody(
required: true,
description: 'Project updated.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Project updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os'],
'name' => ['type' => 'string', 'example' => 'Project Name'],
'description' => ['type' => 'string', 'example' => 'Project Description'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function update_project(Request $request)
{
$allowedFields = ['name', 'description'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|nullable',
'description' => 'string|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$uuid = $request->uuid;
if (! $uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$project->update($request->only($allowedFields));
return response()->json([
'uuid' => $project->uuid,
'name' => $project->name,
'description' => $project->description,
])->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Project deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Project deleted.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function delete_project(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
if ($project->resource_count() > 0) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
$project->delete();
return response()->json(['message' => 'Project deleted.']);
}
}

View File

@ -2,8 +2,12 @@
namespace App\Http\Controllers\Api;
use App\Actions\Server\ValidateServer;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
@ -75,7 +79,7 @@ public function servers(Request $request)
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port', 'description')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
@ -392,4 +396,390 @@ public function domains_by_server(Request $request)
return response()->json(serializeApiResponse($domains));
}
#[OA\Post(
summary: 'Create',
description: 'Create Server.',
path: '/servers',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
requestBody: new OA\RequestBody(
required: true,
description: 'Server created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'example' => 'My Server', 'description' => 'The name of the server.'],
'description' => ['type' => 'string', 'example' => 'My Server Description', 'description' => 'The description of the server.'],
'ip' => ['type' => 'string', 'example' => '127.0.0.1', 'description' => 'The IP of the server.'],
'port' => ['type' => 'integer', 'example' => 22, 'description' => 'The port of the server.'],
'user' => ['type' => 'string', 'example' => 'root', 'description' => 'The user of the server.'],
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Server created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the server.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function create_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|nullable',
'ip' => 'string|required',
'port' => 'integer|nullable',
'private_key_uuid' => 'string|required',
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (! $request->name) {
$request->offsetSet('name', generate_random_name());
}
if (! $request->user) {
$request->offsetSet('user', 'root');
}
if (is_null($request->port)) {
$request->offsetSet('port', 22);
}
if (is_null($request->is_build_server)) {
$request->offsetSet('is_build_server', false);
}
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
}
$allServers = ModelsServer::whereIp($request->ip)->get();
if ($allServers->count() > 0) {
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
'ip' => $request->ip,
'port' => $request->port,
'user' => $request->user,
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
'type' => ProxyTypes::TRAEFIK_V2->value,
'status' => ProxyStatus::EXITED->value,
],
]);
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
if ($request->instant_validate) {
ValidateServer::dispatch($server);
}
return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The name of the server.'],
'description' => ['type' => 'string', 'description' => 'The description of the server.'],
'ip' => ['type' => 'string', 'description' => 'The IP of the server.'],
'port' => ['type' => 'integer', 'description' => 'The port of the server.'],
'user' => ['type' => 'string', 'description' => 'The user of the server.'],
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Server updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function update_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|nullable',
'description' => 'string|nullable',
'ip' => 'string|nullable',
'port' => 'integer|nullable',
'private_key_uuid' => 'string|nullable',
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
}
if ($request->instant_validate) {
ValidateServer::dispatch($server);
}
return response()->json(serializeApiResponse($server))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the server.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Server deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Server deleted.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function delete_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
}
$server->delete();
return response()->json(['message' => 'Server deleted.']);
}
#[OA\Get(
summary: 'Validate',
description: 'Validate server by UUID.',
path: '/servers/{uuid}/validate',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 201,
description: 'Server validation started.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Validation started.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function validate_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']);
}
}

View File

@ -340,7 +340,6 @@ public function normal(Request $request)
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (! $isFunctional) {
@ -432,8 +431,13 @@ public function normal(Request $request)
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$container_name = generateApplicationContainerName($application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {
$container_name = data_get($container, 'Names');
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
});
}
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();

View File

@ -490,10 +490,10 @@ private function deploy_docker_compose_buildpack()
// Start compose file
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
$this->write_deployment_configurations();
} else {
$this->write_deployment_configurations();
$server_workdir = $this->application->workdir();
@ -510,22 +510,21 @@ private function deploy_docker_compose_buildpack()
}
} else {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
$this->write_deployment_configurations();
} else {
$command = "{$this->coolify_variables} docker compose";
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
ray($command);
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
$this->write_deployment_configurations();
}
}
@ -615,14 +614,14 @@ private function write_deployment_configurations()
$this->server = $this->original_server;
}
if (str($this->configuration_dir)->isNotEmpty()) {
ray("docker cp {$this->deployment_uuid}:{$this->workdir} {$this->configuration_dir}");
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir",
],
[
"rm -rf $this->configuration_dir/{*,.*}",
],
// removing this now as we are using docker cp
// [
// "rm -rf $this->configuration_dir/{*,.*}",
// ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
@ -1448,6 +1447,11 @@ private function generate_nixpacks_confs()
}
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
// temporary: disable healthcheck for rust because the start phase does not have curl/wget
$this->application->health_check_enabled = false;
$this->application->save();
}
}
}
}

View File

@ -40,7 +40,7 @@ public function handle()
switch ($this->resource->type()) {
case 'application':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource);
StopApplication::run($this->resource, previewDeployments: true);
break;
case 'standalone-postgresql':
case 'standalone-redis':

View File

@ -257,7 +257,6 @@ public function saveServer()
$this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save();
$this->createdServer->addInitialNetwork();
$this->selectedExistingServer = $this->createdServer->id;
$this->currentState = 'validate-server';
}

View File

@ -164,6 +164,9 @@ public function checkLocalhostConnection()
public function validateServer($install = true)
{
$this->server->update([
'validation_logs' => null,
]);
$this->dispatch('init', $install);
}

View File

@ -124,7 +124,6 @@ public function submit()
}
$server->settings->is_build_server = $this->is_build_server;
$server->settings->save();
$server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {

View File

@ -50,7 +50,7 @@ public function traefikDashboardAvailable(bool $data)
public function proxyStarted()
{
CheckProxy::run($this->server, true);
$this->dispatch('success', 'Proxy started.');
$this->dispatch('proxyStatusUpdated');
}
public function proxyStatusUpdated()
@ -61,7 +61,7 @@ public function proxyStatusUpdated()
public function restart()
{
try {
$this->stop();
$this->stop(forceStop: false);
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@ -91,7 +91,7 @@ public function startProxy()
}
}
public function stop()
public function stop(bool $forceStop = true)
{
try {
if ($this->server->isSwarm()) {
@ -104,7 +104,7 @@ public function stop()
], $this->server);
}
$this->server->proxy->status = 'exited';
$this->server->proxy->force_stop = true;
$this->server->proxy->force_stop = $forceStop;
$this->server->save();
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {

View File

@ -16,7 +16,10 @@ class Status extends Component
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated' => '$refresh', 'startProxyPolling'];
protected $listeners = [
'proxyStatusUpdated',
'startProxyPolling',
];
public function startProxyPolling()
{

View File

@ -87,7 +87,10 @@ public function validateConnection()
{
['uptime' => $this->uptime, 'error' => $error] = $this->server->validateConnection();
if (! $this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br>Error: '.$error;
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
}
@ -99,6 +102,9 @@ public function validateOS()
$this->supported_os_type = $this->server->validateOS();
if (! $this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
}
@ -113,6 +119,9 @@ public function validateDockerEngine()
if ($this->install) {
if ($this->number_of_tries == $this->max_tries) {
$this->error = 'Docker Engine could not be installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
} else {
@ -126,6 +135,9 @@ public function validateDockerEngine()
}
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
}
@ -148,6 +160,9 @@ public function validateDockerVersion()
$this->dispatch('success', 'Server validated.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$this->server->update([
'validation_logs' => $this->error,
]);
return;
}

View File

@ -79,23 +79,29 @@ public function saveStorageOnServer()
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
if ($isFile == 'OK' && $fileVolume->is_directory) {
$fileVolume->is_directory = false;
$fileVolume->save();
throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.');
} elseif ($isDir == 'OK' && ! $fileVolume->is_directory) {
$fileVolume->is_directory = true;
$fileVolume->save();
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.');
}
if (! $fileVolume->is_directory && $isDir == 'NOK') {
if ($isDir == 'NOK' && ! $fileVolume->is_directory) {
$chmod = data_get($fileVolume, 'chmod');
$chown = data_get($fileVolume, 'chown');
if ($content) {
$content = base64_encode($content);
$chmod = $fileVolume->chmod;
$chown = $fileVolume->chown;
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
$commands->push("chmod +x $path");
if ($chown) {
$commands->push("chown $chown $path");
}
if ($chmod) {
$commands->push("chmod $chmod $path");
}
} else {
$commands->push("touch $path");
}
$commands->push("chmod +x $path");
if ($chown) {
$commands->push("chown $chown $path");
}
if ($chmod) {
$commands->push("chmod $chmod $path");
}
} elseif ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");

View File

@ -122,7 +122,7 @@ public function mariadbs()
public function resource_count()
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count();
}
public function databases()

View File

@ -19,85 +19,23 @@
use Symfony\Component\Yaml\Yaml;
#[OA\Schema(
description: 'Application model',
description: 'Server model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'repository_project_id' => ['type' => 'integer', 'nullable' => true],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'fqdn' => ['type' => 'string'],
'config_hash' => ['type' => 'string'],
'git_repository' => ['type' => 'string'],
'git_branch' => ['type' => 'string'],
'git_commit_sha' => ['type' => 'string'],
'git_full_url' => ['type' => 'string', 'nullable' => true],
'docker_registry_image_name' => ['type' => 'string', 'nullable' => true],
'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true],
'build_pack' => ['type' => 'string'],
'static_image' => ['type' => 'string'],
'install_command' => ['type' => 'string'],
'build_command' => ['type' => 'string'],
'start_command' => ['type' => 'string'],
'ports_exposes' => ['type' => 'string'],
'ports_mappings' => ['type' => 'string', 'nullable' => true],
'base_directory' => ['type' => 'string'],
'publish_directory' => ['type' => 'string'],
'health_check_path' => ['type' => 'string'],
'health_check_port' => ['type' => 'string', 'nullable' => true],
'health_check_host' => ['type' => 'string'],
'health_check_method' => ['type' => 'string'],
'health_check_return_code' => ['type' => 'integer'],
'health_check_scheme' => ['type' => 'string'],
'health_check_response_text' => ['type' => 'string', 'nullable' => true],
'health_check_interval' => ['type' => 'integer'],
'health_check_timeout' => ['type' => 'integer'],
'health_check_retries' => ['type' => 'integer'],
'health_check_start_period' => ['type' => 'integer'],
'limits_memory' => ['type' => 'string'],
'limits_memory_swap' => ['type' => 'string'],
'limits_memory_swappiness' => ['type' => 'integer'],
'limits_memory_reservation' => ['type' => 'string'],
'limits_cpus' => ['type' => 'string'],
'limits_cpuset' => ['type' => 'string', 'nullable' => true],
'limits_cpu_shares' => ['type' => 'integer'],
'status' => ['type' => 'string'],
'preview_url_template' => ['type' => 'string'],
'destination_type' => ['type' => 'string'],
'destination_id' => ['type' => 'integer'],
'source_type' => ['type' => 'string'],
'source_id' => ['type' => 'integer'],
'private_key_id' => ['type' => 'integer', 'nullable' => true],
'environment_id' => ['type' => 'integer'],
'created_at' => ['type' => 'string', 'format' => 'date-time'],
'updated_at' => ['type' => 'string', 'format' => 'date-time'],
'description' => ['type' => 'string', 'nullable' => true],
'dockerfile' => ['type' => 'string', 'nullable' => true],
'health_check_enabled' => ['type' => 'boolean'],
'dockerfile_location' => ['type' => 'string'],
'custom_labels' => ['type' => 'string'],
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true],
'docker_compose_location' => ['type' => 'string'],
'docker_compose' => ['type' => 'string', 'nullable' => true],
'docker_compose_raw' => ['type' => 'string', 'nullable' => true],
'docker_compose_domains' => ['type' => 'string', 'nullable' => true],
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true],
'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true],
'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true],
'swarm_replicas' => ['type' => 'integer'],
'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true],
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true],
'custom_docker_run_options' => ['type' => 'string', 'nullable' => true],
'post_deployment_command' => ['type' => 'string', 'nullable' => true],
'post_deployment_command_container' => ['type' => 'string', 'nullable' => true],
'pre_deployment_command' => ['type' => 'string', 'nullable' => true],
'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true],
'watch_paths' => ['type' => 'string', 'nullable' => true],
'custom_healthcheck_found' => ['type' => 'boolean'],
'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true],
'redirect' => ['type' => 'string'],
'description' => ['type' => 'string'],
'ip' => ['type' => 'string'],
'user' => ['type' => 'string'],
'port' => ['type' => 'integer'],
'proxy' => ['type' => 'object'],
'high_disk_usage_notification_sent' => ['type' => 'boolean'],
'unreachable_notification_sent' => ['type' => 'boolean'],
'unreachable_count' => ['type' => 'integer'],
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'],
]
)]
@ -123,6 +61,37 @@ protected static function booted()
ServerSetting::create([
'server_id' => $server->id,
]);
if ($server->id === 0) {
if ($server->isSwarm()) {
SwarmDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify-overlay',
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
} else {
if ($server->isSwarm()) {
SwarmDocker::create([
'name' => 'coolify-overlay',
'network' => 'coolify-overlay',
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $server->id,
]);
}
}
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
@ -176,41 +145,6 @@ public function settings()
return $this->hasOne(ServerSetting::class);
}
public function addInitialNetwork()
{
if ($this->id === 0) {
if ($this->isSwarm()) {
SwarmDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify-overlay',
'server_id' => $this->id,
]);
} else {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $this->id,
]);
}
} else {
if ($this->isSwarm()) {
SwarmDocker::create([
'name' => 'coolify-overlay',
'network' => 'coolify-overlay',
'server_id' => $this->id,
]);
} else {
StandaloneDocker::create([
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $this->id,
]);
}
}
}
public function setupDefault404Redirect()
{
$dynamic_conf_path = $this->proxyPath().'/dynamic';

View File

@ -73,6 +73,13 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
"echo '$content' | base64 -d | tee $fileLocation",
], $server);
} elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
// Does not exists (no dir or file), flagged as directory, is init
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
} elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && ! $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();

View File

@ -1735,6 +1735,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if (is_array($volume)) {
return data_get($volume, 'source');
}
dispatch(new ServerFilesFromServerJob($resource));
return $volume->value();
});

View File

@ -7,7 +7,7 @@
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.317',
'release' => '4.0.0-beta.318',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.317';
return '4.0.0-beta.318';

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->boolean('health_check_enabled')->default(false)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->boolean('health_check_enabled')->default(true)->change();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->text('validation_logs')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('validation_logs');
});
}
};

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->string('git_repository')->nullable();
$table->string('git_branch')->nullable();
$table->nullableMorphs('source');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropColumn('git_repository');
$table->dropColumn('git_branch');
$table->dropMorphs('source');
});
}
};

View File

@ -3010,6 +3010,44 @@ paths:
security:
-
bearerAuth: []
post:
tags:
- Projects
summary: Create
description: 'Create Project.'
operationId: cf067eb7cf18216cda3239329a2eeadb
requestBody:
description: 'Project created.'
required: true
content:
application/json:
schema:
properties:
uuid:
type: string
description: 'The name of the project.'
description:
type: string
description: 'The description of the project.'
type: object
responses:
'201':
description: 'Project created.'
content:
application/json:
schema:
properties:
uuid: { type: string, example: og888os, description: 'The UUID of the project.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
'/projects/{uuid}':
get:
tags:
@ -3041,6 +3079,79 @@ paths:
security:
-
bearerAuth: []
delete:
tags:
- Projects
summary: Delete
description: 'Delete project by UUID.'
operationId: f668a936f505b4401948c74b6a663029
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
responses:
'200':
description: 'Project deleted.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Project deleted.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
patch:
tags:
- Projects
summary: Update
description: 'Update Project.'
operationId: 2db343bd6fc14c658cb51a2b73b2f842
requestBody:
description: 'Project updated.'
required: true
content:
application/json:
schema:
properties:
name:
type: string
description: 'The name of the project.'
description:
type: string
description: 'The description of the project.'
type: object
responses:
'201':
description: 'Project updated.'
content:
application/json:
schema:
properties:
uuid: { type: string, example: og888os }
name: { type: string, example: 'Project Name' }
description: { type: string, example: 'Project Description' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
'/projects/{uuid}/{environment_name}':
get:
tags:
@ -3288,6 +3399,70 @@ paths:
security:
-
bearerAuth: []
post:
tags:
- Servers
summary: Create
description: 'Create Server.'
operationId: fa44b42490379e428ba5b8747716a8d9
requestBody:
description: 'Server created.'
required: true
content:
application/json:
schema:
properties:
name:
type: string
example: 'My Server'
description: 'The name of the server.'
description:
type: string
example: 'My Server Description'
description: 'The description of the server.'
ip:
type: string
example: 127.0.0.1
description: 'The IP of the server.'
port:
type: integer
example: 22
description: 'The port of the server.'
user:
type: string
example: root
description: 'The user of the server.'
private_key_uuid:
type: string
example: og888os
description: 'The UUID of the private key.'
is_build_server:
type: boolean
example: false
description: 'Is build server.'
instant_validate:
type: boolean
example: false
description: 'Instant validate.'
type: object
responses:
'201':
description: 'Server created.'
content:
application/json:
schema:
properties:
uuid: { type: string, example: og888os, description: 'The UUID of the server.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
'/servers/{uuid}':
get:
tags:
@ -3319,6 +3494,95 @@ paths:
security:
-
bearerAuth: []
delete:
tags:
- Servers
summary: Delete
description: 'Delete server by UUID.'
operationId: 0231fe0134f0306b21f006ce51b0a3dc
parameters:
-
name: uuid
in: path
description: 'UUID of the server.'
required: true
schema:
type: string
format: uuid
responses:
'200':
description: 'Server deleted.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Server deleted.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
patch:
tags:
- Servers
summary: Update
description: 'Update Server.'
operationId: 41bbdaf79eb1938592494fc5494442a0
requestBody:
description: 'Server updated.'
required: true
content:
application/json:
schema:
properties:
name:
type: string
description: 'The name of the server.'
description:
type: string
description: 'The description of the server.'
ip:
type: string
description: 'The IP of the server.'
port:
type: integer
description: 'The port of the server.'
user:
type: string
description: 'The user of the server.'
private_key_uuid:
type: string
description: 'The UUID of the private key.'
is_build_server:
type: boolean
description: 'Is build server.'
instant_validate:
type: boolean
description: 'Instant validate.'
type: object
responses:
'201':
description: 'Server updated.'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Server'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
'/servers/{uuid}/resources':
get:
tags:
@ -3383,6 +3647,39 @@ paths:
security:
-
bearerAuth: []
'/servers/{uuid}/validate':
get:
tags:
- Servers
summary: Validate
description: 'Validate server by UUID.'
operationId: a543a12ef2cbc7a3dd22c3dbe6cbee89
parameters:
-
name: uuid
in: path
description: 'Server UUID'
required: true
schema:
type: integer
responses:
'201':
description: 'Server validation started.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Validation started.' }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
/services:
get:
tags:
@ -4189,191 +4486,35 @@ components:
$ref: '#/components/schemas/Environment'
type: object
Server:
description: 'Application model'
description: 'Server model'
properties:
id:
type: integer
repository_project_id:
type: integer
nullable: true
uuid:
type: string
name:
type: string
fqdn:
type: string
config_hash:
type: string
git_repository:
type: string
git_branch:
type: string
git_commit_sha:
type: string
git_full_url:
type: string
nullable: true
docker_registry_image_name:
type: string
nullable: true
docker_registry_image_tag:
type: string
nullable: true
build_pack:
type: string
static_image:
type: string
install_command:
type: string
build_command:
type: string
start_command:
type: string
ports_exposes:
type: string
ports_mappings:
type: string
nullable: true
base_directory:
type: string
publish_directory:
type: string
health_check_path:
type: string
health_check_port:
type: string
nullable: true
health_check_host:
type: string
health_check_method:
type: string
health_check_return_code:
type: integer
health_check_scheme:
type: string
health_check_response_text:
type: string
nullable: true
health_check_interval:
type: integer
health_check_timeout:
type: integer
health_check_retries:
type: integer
health_check_start_period:
type: integer
limits_memory:
type: string
limits_memory_swap:
type: string
limits_memory_swappiness:
type: integer
limits_memory_reservation:
type: string
limits_cpus:
type: string
limits_cpuset:
type: string
nullable: true
limits_cpu_shares:
type: integer
status:
type: string
preview_url_template:
type: string
destination_type:
type: string
destination_id:
type: integer
source_type:
type: string
source_id:
type: integer
private_key_id:
type: integer
nullable: true
environment_id:
type: integer
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
description:
type: string
nullable: true
dockerfile:
ip:
type: string
nullable: true
health_check_enabled:
type: boolean
dockerfile_location:
user:
type: string
custom_labels:
type: string
dockerfile_target_build:
type: string
nullable: true
manual_webhook_secret_github:
type: string
nullable: true
manual_webhook_secret_gitlab:
type: string
nullable: true
docker_compose_location:
type: string
docker_compose:
type: string
nullable: true
docker_compose_raw:
type: string
nullable: true
docker_compose_domains:
type: string
nullable: true
deleted_at:
type: string
format: date-time
nullable: true
docker_compose_custom_start_command:
type: string
nullable: true
docker_compose_custom_build_command:
type: string
nullable: true
swarm_replicas:
port:
type: integer
swarm_placement_constraints:
type: string
nullable: true
manual_webhook_secret_bitbucket:
type: string
nullable: true
custom_docker_run_options:
type: string
nullable: true
post_deployment_command:
type: string
nullable: true
post_deployment_command_container:
type: string
nullable: true
pre_deployment_command:
type: string
nullable: true
pre_deployment_command_container:
type: string
nullable: true
watch_paths:
type: string
nullable: true
custom_healthcheck_found:
proxy:
type: object
high_disk_usage_notification_sent:
type: boolean
manual_webhook_secret_gitea:
unreachable_notification_sent:
type: boolean
unreachable_count:
type: integer
validation_logs:
type: string
nullable: true
redirect:
log_drain_notification_sent:
type: boolean
swarm_cluster:
type: string
type: object
ServerSetting:
@ -4480,6 +4621,9 @@ components:
is_container_label_escape_enabled:
type: boolean
description: 'The flag to enable the container label escape.'
is_container_label_readonly_enabled:
type: boolean
description: 'The flag to enable the container label readonly.'
config_hash:
type: string
description: 'The hash of the service configuration.'

BIN
other/logos/branddev.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -18,7 +18,7 @@
</div>
@if (!str($status)->startsWith('Proxy') && !str($status)->contains('('))
@if (str($status)->contains('unhealthy'))
<x-helper helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>This doesn't mean that the resource is malfunctioning.</span><br><br>- If the resource is accessible, it indicates that no health check is configured - it is not mandatory.<br>- If the resource is not accessible (returning 404 or 503), it may indicate that a health check is needed and has not passed. <span class='dark:text-warning text-coollabs'>Your action is required.</span>" >
<x-helper helper="Unhealthy state. <span class='dark:text-warning text-coollabs'>This doesn't mean that the resource is malfunctioning.</span><br><br>- If the resource is accessible, it indicates that no health check is configured - it is not mandatory.<br>- If the resource is not accessible (returning 404 or 503), it may indicate that a health check is needed and has not passed. <span class='dark:text-warning text-coollabs'>Your action is required.</span><br><br>More details in the <a href='https://coolify.io/docs/knowledge-base/healthchecks' class='underline dark:text-warning text-coollabs' target='_blank'>documentation</a>." >
<x-slot:icon>
<svg class="hidden w-4 h-4 dark:text-warning lg:block" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16"></path>

View File

@ -106,7 +106,7 @@
<livewire:project.shared.destination :resource="$application" :servers="$servers" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.service.storage :resource="$application" />
<livewire:project.service.storage :resource="$application" lazy />
</div>
<div x-cloak x-show="activeTab === 'webhooks'">
<livewire:project.shared.webhooks :resource="$application" lazy />

View File

@ -175,15 +175,15 @@ class="w-4 h-4 dark:text-warning text-coollabs"
<div class="pb-4 dark:text-warning text-coollabs">If you would like to add a volume, you must add it to
your compose file (General tab).</div>
@foreach ($applications as $application)
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
:resource="$application" />
<livewire:project.service.storage wire:key="application-{{ $application->id }}" :resource="$application"
lazy />
@endforeach
@foreach ($databases as $database)
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" />
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" lazy />
@endforeach
</div>
<div x-cloak x-show="activeTab === 'scheduled-tasks'">
<livewire:project.shared.scheduled-task.all :resource="$service" />
<livewire:project.shared.scheduled-task.all :resource="$service" lazy />
</div>
<div x-cloak x-show="activeTab === 'webhooks'">
<livewire:project.shared.webhooks :resource="$service" />

View File

@ -8,25 +8,35 @@
<h3 class="pt-4">Limit CPUs</h3>
<div class="flex gap-2">
<x-forms.input placeholder="1.5"
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a class='dark:text-white underline' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
helper="0 means use all CPUs. Floating point number, like 0.002 or 1.5. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="Number of CPUs" id="resource.limits_cpus" />
<x-forms.input placeholder="0-2"
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a class='dark:text-white underline' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
helper="Empty means, use all CPU sets. 0-2 will use CPU 0, CPU 1 and CPU 2. More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="CPU sets to use" id="resource.limits_cpuset" />
<x-forms.input placeholder="1024"
helper="More info <a class='dark:text-white underline' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
helper="More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/engine/reference/run/#cpu-share-constraint'>here</a>."
label="CPU Weight" id="resource.limits_cpu_shares" />
</div>
<h3 class="pt-4">Limit Memory</h3>
<div class="flex gap-2">
<x-forms.input placeholder="69b or 420k or 1337m or 1g" label="Soft Memory Limit"
id="resource.limits_memory_reservation" />
<x-forms.input placeholder="69b or 420k or 1337m or 1g" label="Maximum Memory Limit"
id="resource.limits_memory" />
<x-forms.input placeholder="69b or 420k or 1337m or 1g" label="Maximum Swap Limit"
id="resource.limits_memory_swap" />
<x-forms.input placeholder="0-100" type="number" min="0" max="100" label="Swappiness"
id="resource.limits_memory_swappiness" />
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_reservation'>here</a>."
label="Soft Memory Limit" id="resource.limits_memory_reservation" />
<x-forms.input
helper="0-100.<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_swappiness'>here</a>."
type="number" min="0" max="100" label="Swappiness"
id="resource.limits_memory_swappiness" />
</div>
<div class="flex gap-2">
<x-forms.input
helper="Examples: 69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#mem_limit'>here</a>."
label="Maximum Memory Limit" id="resource.limits_memory" />
<x-forms.input
helper="Examples:69b (byte) or 420k (kilobyte) or 1337m (megabyte) or 1g (gigabyte).<br>More info <a class='underline dark:text-white' target='_blank' href='https://docs.docker.com/compose/compose-file/05-services/#memswap_limit'>here</a>."
label="Maximum Swap Limit" id="resource.limits_memory_swap" />
</div>
</div>
</form>
</div>

View File

@ -40,6 +40,12 @@ class="w-full mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-1
Validate Server & Install Docker Engine
</x-forms.button>
</x-slide-over>
@if ($server->validation_logs)
<h4>Previous Validation Logs</h4>
<div class="pb-8">
{!! $server->validation_logs !!}
</div>
@endif
@endif
@if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0)
<x-forms.button class="mt-8 mb-4 font-bold box-without-bg bg-coollabs hover:bg-coollabs-100"

View File

@ -41,6 +41,10 @@
Route::get('/projects/{uuid}', [ProjectController::class, 'project_by_uuid']);
Route::get('/projects/{uuid}/{environment_name}', [ProjectController::class, 'environment_details']);
Route::post('/projects', [ProjectController::class, 'create_project']);
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project']);
Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project']);
Route::get('/security/keys', [SecurityController::class, 'keys']);
Route::post('/security/keys', [SecurityController::class, 'create_key'])->middleware([IgnoreReadOnlyApiToken::class]);
@ -57,6 +61,12 @@
Route::get('/servers/{uuid}/domains', [ServersController::class, 'domains_by_server']);
Route::get('/servers/{uuid}/resources', [ServersController::class, 'resources_by_server']);
Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server']);
Route::post('/servers', [ServersController::class, 'create_server']);
Route::patch('/servers/{uuid}', [ServersController::class, 'update_server']);
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server']);
Route::get('/resources', [ResourcesController::class, 'resources']);
Route::get('/applications', [ApplicationsController::class, 'applications']);

View File

@ -5,9 +5,9 @@
x-app-env: &app-env
environment:
- WEB_URL=http://localhost
- WEB_URL=${SERVICE_FQDN_PLANE}
- DEBUG=${DEBUG:-0}
- CORS_ALLOWED_ORIGINS=http://localhost
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGIN:-http://localhost}
# Gunicorn Workers
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
#DB SETTINGS
@ -28,8 +28,8 @@ x-app-env: &app-env
# DATA STORE SETTINGS
- USE_MINIO=${USE_MINIO:-1}
- AWS_REGION=${AWS_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_ACCESS_KEY_ID=$SERVICE_USER_MINIO
- AWS_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
@ -39,13 +39,14 @@ x-app-env: &app-env
# Admin and Space URLs
- ADMIN_BASE_URL=${ADMIN_BASE_URL}
- SPACE_BASE_URL=${SPACE_BASE_URL}
- APP_BASE_URL=${APP_BASE_URL}
- APP_BASE_URL=${SERVICE_FQDN_PLANE}
services:
proxy:
environment:
- SERVICE_FQDN_PLANE
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
image: makeplane/plane-proxy:stable
depends_on:
- web

View File

@ -301,7 +301,7 @@ services:
- DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization}
- DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project}
- SUPABASE_URL=${SERVICE_FQDN_SUPABASEKONG:-http://supabase-kong:8000}
- SUPABASE_URL=${SERVICE_FQDN_SUPABASEKONG}
- SUPABASE_PUBLIC_URL=${SERVICE_FQDN_SUPABASEKONG}
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY}
@ -1013,7 +1013,7 @@ services:
"/dev/null",
"-H",
"Authorization: Bearer ${SERVICE_SUPABASEANON_KEY}",
"http://127.0.0.1:4000/api/tenants/realtime-dev/health"
"http://127.0.0.1:4000/api/tenants/realtime-dev/health",
]
timeout: 5s
interval: 5s
@ -1182,7 +1182,7 @@ services:
retries: 3
environment:
- JWT_SECRET=${SERVICE_PASSWORD_JWT}
- SUPABASE_URL=${SERVICE_FQDN_SUPABASEKONG:-http://supabase-kong:8000}
- SUPABASE_URL=${SERVICE_FQDN_SUPABASEKONG}
- SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY}
- SUPABASE_SERVICE_ROLE_KEY=${SERVICE_SUPABASESERVICE_KEY}
- SUPABASE_DB_URL=postgresql://postgres:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.317"
"version": "4.0.0-beta.318"
}
}
}