diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2d354057e..4f12f436c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,5 @@ contact_links: url: https://coollabs.io/discord about: Reach out to us on Discord. - name: 🙋‍♂️ Feature Requests - url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas + url: https://github.com/coollabsio/coolify/discussions/categories/new-features about: All feature requests will be discussed here. diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues similarity index 100% rename from .github/workflows/fix-php-code-style-issues.yml rename to .github/workflows/fix-php-code-style-issues diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 000000000..cf2fae8f3 --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,81 @@ +name: PR Build (v4) + +on: + pull_request: + types: + - opened + branches-ignore: ["main", "v3"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - docker/coolify-helper/Dockerfile + +env: + REGISTRY: ghcr.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }} + aarch64: + runs-on: [self-hosted, arm64] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build image and push to registry + uses: docker/build-push-action@v5 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to ghcr.io + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Create & publish manifest + run: | + docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }} + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 1f05e29ac..7155f9a0a 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -31,15 +31,13 @@ class StopApplication } else { $containers = getCurrentApplicationContainerStatus($server, $application->id, 0); } - ray($containers); if ($containers->count() > 0) { foreach ($containers as $container) { $containerName = data_get($container, 'Names'); if ($containerName) { - instant_remote_process( - ["docker rm -f {$containerName}"], - $server - ); + instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false); } } } diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index e4903ff35..d562ec56f 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -22,10 +22,11 @@ class StopDatabase if (! $server->isFunctional()) { return 'Server is not functional'; } - instant_remote_process( - ["docker rm -f {$database->uuid}"], - $server - ); + + instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false); + if ($database->is_public) { StopDatabaseProxy::run($database); } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 9b32e89f3..fdaa88ebf 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -12,6 +12,7 @@ use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerStopped; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Lorisleiva\Actions\Concerns\AsAction; class GetContainersStatus @@ -20,13 +21,16 @@ class GetContainersStatus public $applications; + public ?Collection $containers; + + public ?Collection $containerReplicates; + public $server; - public function handle(Server $server) + public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null) { - // if (isDev()) { - // $server = Server::find(0); - // } + $this->containers = $containers; + $this->containerReplicates = $containerReplicates; $this->server = $server; if (! $this->server->isFunctional()) { return 'Server is not ready.'; @@ -66,322 +70,312 @@ class GetContainersStatus // } } - private function sentinel() - { - try { - $containers = $this->server->getContainers(); - if ($containers->count() === 0) { - return; - } - $databases = $this->server->databases(); - $services = $this->server->services()->get(); - $previews = $this->server->previews(); - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; + // private function sentinel() + // { + // try { + // $this->containers = $this->server->getContainersWithSentinel(); + // if ($this->containers->count() === 0) { + // return; + // } + // $databases = $this->server->databases(); + // $services = $this->server->services()->get(); + // $previews = $this->server->previews(); + // $foundApplications = []; + // $foundApplicationPreviews = []; + // $foundDatabases = []; + // $foundServices = []; - foreach ($containers as $container) { - $labels = Arr::undot(data_get($container, 'labels')); - $containerStatus = data_get($container, 'state'); - $containerHealth = data_get($container, 'health_status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; - $applicationId = data_get($labels, 'coolify.applicationId'); - if ($applicationId) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - if ($pullRequestId) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $foundApplicationPreviews[] = $preview->id; - $statusFromDb = $preview->status; - if ($statusFromDb !== $containerStatus) { - $preview->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } else { - $application = $this->applications->where('id', $applicationId)->first(); - if ($application) { - $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } - } else { - $uuid = data_get($labels, 'com.docker.compose.service'); - $type = data_get($labels, 'coolify.type'); - if ($uuid) { - if ($type === 'service') { - $database_id = data_get($labels, 'coolify.service.subId'); - if ($database_id) { - $service_db = ServiceDatabase::where('id', $database_id)->first(); - if ($service_db) { - $uuid = $service_db->service->uuid; - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'name') === "$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - } - } - } - } - } else { - $database = $databases->where('uuid', $uuid)->first(); - if ($database) { - $isPublic = data_get($database, 'is_public'); - $foundDatabases[] = $database->id; - $statusFromDb = $database->status; - if ($statusFromDb !== $containerStatus) { - $database->update(['status' => $containerStatus]); - } - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'name') === "$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - } - } - } else { - // Notify user that this container should not be there. - } - } - } - if (data_get($container, 'name') === 'coolify-db') { - $foundDatabases[] = 0; - } - } - $serviceLabelId = data_get($labels, 'coolify.serviceId'); - if ($serviceLabelId) { - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = $services->where('id', $serviceLabelId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); - } else { - $service = $service->databases()->where('id', $subId)->first(); - } - if ($service) { - $foundServices[] = "$service->id-$service->name"; - $statusFromDb = $service->status; - if ($statusFromDb !== $containerStatus) { - // ray('Updating status: ' . $containerStatus); - $service->update(['status' => $containerStatus]); - } - } - } - } - $exitedServices = collect([]); - foreach ($services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - if (in_array("$app->id-$app->name", $foundServices)) { - continue; - } else { - $exitedServices->push($app); - } - } - foreach ($dbs as $db) { - if (in_array("$db->id-$db->name", $foundServices)) { - continue; - } else { - $exitedServices->push($db); - } - } - } - $exitedServices = $exitedServices->unique('id'); - foreach ($exitedServices as $exitedService) { - if (str($exitedService->status)->startsWith('exited')) { - continue; - } - $name = data_get($exitedService, 'name'); - $fqdn = data_get($exitedService, 'fqdn'); - if ($name) { - if ($fqdn) { - $containerName = "$name, available at $fqdn"; - } else { - $containerName = $name; - } - } else { - if ($fqdn) { - $containerName = $fqdn; - } else { - $containerName = null; - } - } - $projectUuid = data_get($service, 'environment.project.uuid'); - $serviceUuid = data_get($service, 'uuid'); - $environmentName = data_get($service, 'environment.name'); + // foreach ($this->containers as $container) { + // $labels = Arr::undot(data_get($container, 'labels')); + // $containerStatus = data_get($container, 'state'); + // $containerHealth = data_get($container, 'health_status', 'unhealthy'); + // $containerStatus = "$containerStatus ($containerHealth)"; + // $applicationId = data_get($labels, 'coolify.applicationId'); + // if ($applicationId) { + // $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + // if ($pullRequestId) { + // if (str($applicationId)->contains('-')) { + // $applicationId = str($applicationId)->before('-'); + // } + // $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + // if ($preview) { + // $foundApplicationPreviews[] = $preview->id; + // $statusFromDb = $preview->status; + // if ($statusFromDb !== $containerStatus) { + // $preview->update(['status' => $containerStatus]); + // } + // } else { + // //Notify user that this container should not be there. + // } + // } else { + // $application = $this->applications->where('id', $applicationId)->first(); + // if ($application) { + // $foundApplications[] = $application->id; + // $statusFromDb = $application->status; + // if ($statusFromDb !== $containerStatus) { + // $application->update(['status' => $containerStatus]); + // } + // } else { + // //Notify user that this container should not be there. + // } + // } + // } else { + // $uuid = data_get($labels, 'com.docker.compose.service'); + // $type = data_get($labels, 'coolify.type'); + // if ($uuid) { + // if ($type === 'service') { + // $database_id = data_get($labels, 'coolify.service.subId'); + // if ($database_id) { + // $service_db = ServiceDatabase::where('id', $database_id)->first(); + // if ($service_db) { + // $uuid = $service_db->service->uuid; + // $isPublic = data_get($service_db, 'is_public'); + // if ($isPublic) { + // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + // if ($this->server->isSwarm()) { + // // TODO: fix this with sentinel + // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + // } else { + // return data_get($value, 'name') === "$uuid-proxy"; + // } + // })->first(); + // if (! $foundTcpProxy) { + // StartDatabaseProxy::run($service_db); + // // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); + // } + // } + // } + // } + // } else { + // $database = $databases->where('uuid', $uuid)->first(); + // if ($database) { + // $isPublic = data_get($database, 'is_public'); + // $foundDatabases[] = $database->id; + // $statusFromDb = $database->status; + // if ($statusFromDb !== $containerStatus) { + // $database->update(['status' => $containerStatus]); + // } + // if ($isPublic) { + // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + // if ($this->server->isSwarm()) { + // // TODO: fix this with sentinel + // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + // } else { + // return data_get($value, 'name') === "$uuid-proxy"; + // } + // })->first(); + // if (! $foundTcpProxy) { + // StartDatabaseProxy::run($database); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // } + // } + // } else { + // // Notify user that this container should not be there. + // } + // } + // } + // if (data_get($container, 'name') === 'coolify-db') { + // $foundDatabases[] = 0; + // } + // } + // $serviceLabelId = data_get($labels, 'coolify.serviceId'); + // if ($serviceLabelId) { + // $subType = data_get($labels, 'coolify.service.subType'); + // $subId = data_get($labels, 'coolify.service.subId'); + // $service = $services->where('id', $serviceLabelId)->first(); + // if (! $service) { + // continue; + // } + // if ($subType === 'application') { + // $service = $service->applications()->where('id', $subId)->first(); + // } else { + // $service = $service->databases()->where('id', $subId)->first(); + // } + // if ($service) { + // $foundServices[] = "$service->id-$service->name"; + // $statusFromDb = $service->status; + // if ($statusFromDb !== $containerStatus) { + // // ray('Updating status: ' . $containerStatus); + // $service->update(['status' => $containerStatus]); + // } + // } + // } + // } + // $exitedServices = collect([]); + // foreach ($services as $service) { + // $apps = $service->applications()->get(); + // $dbs = $service->databases()->get(); + // foreach ($apps as $app) { + // if (in_array("$app->id-$app->name", $foundServices)) { + // continue; + // } else { + // $exitedServices->push($app); + // } + // } + // foreach ($dbs as $db) { + // if (in_array("$db->id-$db->name", $foundServices)) { + // continue; + // } else { + // $exitedServices->push($db); + // } + // } + // } + // $exitedServices = $exitedServices->unique('id'); + // foreach ($exitedServices as $exitedService) { + // if (str($exitedService->status)->startsWith('exited')) { + // continue; + // } + // $name = data_get($exitedService, 'name'); + // $fqdn = data_get($exitedService, 'fqdn'); + // if ($name) { + // if ($fqdn) { + // $containerName = "$name, available at $fqdn"; + // } else { + // $containerName = $name; + // } + // } else { + // if ($fqdn) { + // $containerName = $fqdn; + // } else { + // $containerName = null; + // } + // } + // $projectUuid = data_get($service, 'environment.project.uuid'); + // $serviceUuid = data_get($service, 'uuid'); + // $environmentName = data_get($service, 'environment.name'); - if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - $exitedService->update(['status' => 'exited']); - } + // if ($projectUuid && $serviceUuid && $environmentName) { + // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; + // } else { + // $url = null; + // } + // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + // $exitedService->update(['status' => 'exited']); + // } - $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $this->applications->where('id', $applicationId)->first(); - if (str($application->status)->startsWith('exited')) { - continue; - } - $application->update(['status' => 'exited']); + // $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); + // foreach ($notRunningApplications as $applicationId) { + // $application = $this->applications->where('id', $applicationId)->first(); + // if (str($application->status)->startsWith('exited')) { + // continue; + // } + // $application->update(['status' => 'exited']); - $name = data_get($application, 'name'); - $fqdn = data_get($application, 'fqdn'); + // $name = data_get($application, 'name'); + // $fqdn = data_get($application, 'fqdn'); - $containerName = $name ? "$name ($fqdn)" : $fqdn; + // $containerName = $name ? "$name ($fqdn)" : $fqdn; - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); + // $projectUuid = data_get($application, 'environment.project.uuid'); + // $applicationUuid = data_get($application, 'uuid'); + // $environment = data_get($application, 'environment.name'); - if ($projectUuid && $applicationUuid && $environment) { - $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - } else { - $url = null; - } + // if ($projectUuid && $applicationUuid && $environment) { + // $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; + // } else { + // $url = null; + // } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $previews->where('id', $previewId)->first(); - if (str($preview->status)->startsWith('exited')) { - continue; - } - $preview->update(['status' => 'exited']); + // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + // } + // $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); + // foreach ($notRunningApplicationPreviews as $previewId) { + // $preview = $previews->where('id', $previewId)->first(); + // if (str($preview->status)->startsWith('exited')) { + // continue; + // } + // $preview->update(['status' => 'exited']); - $name = data_get($preview, 'name'); - $fqdn = data_get($preview, 'fqdn'); + // $name = data_get($preview, 'name'); + // $fqdn = data_get($preview, 'fqdn'); - $containerName = $name ? "$name ($fqdn)" : $fqdn; + // $containerName = $name ? "$name ($fqdn)" : $fqdn; - $projectUuid = data_get($preview, 'application.environment.project.uuid'); - $environmentName = data_get($preview, 'application.environment.name'); - $applicationUuid = data_get($preview, 'application.uuid'); + // $projectUuid = data_get($preview, 'application.environment.project.uuid'); + // $environmentName = data_get($preview, 'application.environment.name'); + // $applicationUuid = data_get($preview, 'application.uuid'); - if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - } else { - $url = null; - } + // if ($projectUuid && $applicationUuid && $environmentName) { + // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; + // } else { + // $url = null; + // } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $databases->where('id', $database)->first(); - if (str($database->status)->startsWith('exited')) { - continue; - } - $database->update(['status' => 'exited']); + // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + // } + // $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); + // foreach ($notRunningDatabases as $database) { + // $database = $databases->where('id', $database)->first(); + // if (str($database->status)->startsWith('exited')) { + // continue; + // } + // $database->update(['status' => 'exited']); - $name = data_get($database, 'name'); - $fqdn = data_get($database, 'fqdn'); + // $name = data_get($database, 'name'); + // $fqdn = data_get($database, 'fqdn'); - $containerName = $name; + // $containerName = $name; - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); + // $projectUuid = data_get($database, 'environment.project.uuid'); + // $environmentName = data_get($database, 'environment.name'); + // $databaseUuid = data_get($database, 'uuid'); - if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } + // if ($projectUuid && $databaseUuid && $environmentName) { + // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; + // } else { + // $url = null; + // } + // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + // } - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - // TODO: fix this with sentinel - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'name') === 'coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'state'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } - } catch (\Exception $e) { - // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); - ray($e->getMessage()); + // // Check if proxy is running + // $this->server->proxyType(); + // $foundProxyContainer = $this->containers->filter(function ($value, $key) { + // if ($this->server->isSwarm()) { + // // TODO: fix this with sentinel + // return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + // } else { + // return data_get($value, 'name') === 'coolify-proxy'; + // } + // })->first(); + // if (! $foundProxyContainer) { + // try { + // $shouldStart = CheckProxy::run($this->server); + // if ($shouldStart) { + // StartProxy::run($this->server, false); + // $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + // } + // } catch (\Throwable $e) { + // ray($e); + // } + // } else { + // $this->server->proxy->status = data_get($foundProxyContainer, 'state'); + // $this->server->save(); + // $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + // instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + // } + // } catch (\Exception $e) { + // // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); + // ray($e->getMessage()); - return handleError($e); - } - } + // return handleError($e); + // } + // } private function old_way() { - if ($this->server->isSwarm()) { - $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); - } else { - // Precheck for containers - $containers = instant_remote_process(['docker container ls -q'], $this->server, false); - if (! $containers) { - return; - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); - $containerReplicates = null; + if ($this->containers === null) { + ['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); } - if (is_null($containers)) { + + if (is_null($this->containers)) { return; } - $containers = format_docker_command_output_to_json($containers); - if ($containerReplicates) { - $containerReplicates = format_docker_command_output_to_json($containerReplicates); - foreach ($containerReplicates as $containerReplica) { + if ($this->containerReplicates) { + foreach ($this->containerReplicates as $containerReplica) { $name = data_get($containerReplica, 'Name'); - $containers = $containers->map(function ($container) use ($name, $containerReplica) { + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { if (data_get($container, 'Spec.Name') === $name) { $replicas = data_get($containerReplica, 'Replicas'); $running = str($replicas)->explode('/')[0]; @@ -407,7 +401,7 @@ class GetContainersStatus $foundDatabases = []; $foundServices = []; - foreach ($containers as $container) { + foreach ($this->containers as $container) { if ($this->server->isSwarm()) { $labels = data_get($container, 'Spec.Labels'); $uuid = data_get($labels, 'coolify.name'); @@ -461,7 +455,7 @@ class GetContainersStatus if ($uuid) { $isPublic = data_get($service_db, 'is_public'); if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { @@ -486,7 +480,7 @@ class GetContainersStatus $database->update(['status' => $containerStatus]); } if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { @@ -659,7 +653,7 @@ class GetContainersStatus // Check if proxy is running $this->server->proxyType(); - $foundProxyContainer = $containers->filter(function ($value, $key) { + $foundProxyContainer = $this->containers->filter(function ($value, $key) { if ($this->server->isSwarm()) { return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; } else { diff --git a/app/Actions/Server/CleanupDocker.php b/app/Actions/Server/CleanupDocker.php index 35cab4d11..0009e001d 100644 --- a/app/Actions/Server/CleanupDocker.php +++ b/app/Actions/Server/CleanupDocker.php @@ -11,7 +11,6 @@ class CleanupDocker public function handle(Server $server, bool $force = true) { - // cleanup docker images, containers, and builder caches if ($force) { instant_remote_process(['docker image prune -af'], $server, false); @@ -22,15 +21,5 @@ class CleanupDocker instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false); instant_remote_process(['docker builder prune -f'], $server, false); } - // cleanup networks - // $networks = collectDockerNetworksByServer($server); - // $proxyNetworks = collectProxyDockerNetworksByServer($server); - // $diff = $proxyNetworks->diff($networks); - // if ($diff->count() > 0) { - // $diff->map(function ($network) use ($server) { - // instant_remote_process(["docker network disconnect $network coolify-proxy"], $server); - // instant_remote_process(["docker network rm $network"], $server); - // }); - // } } } diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/InstallLogDrain.php index 6f74e020b..034d89fe7 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/InstallLogDrain.php @@ -24,12 +24,7 @@ class InstallLogDrain } try { if ($type === 'none') { - $command = [ - "echo 'Stopping old Fluent Bit'", - 'docker rm -f coolify-log-drain || true', - ]; - - return instant_remote_process($command, $server); + return 'No log drain is enabled.'; } elseif ($type === 'newrelic') { if (! $server->settings->is_logdrain_newrelic_enabled) { throw new \Exception('New Relic log drain is not enabled.'); diff --git a/app/Actions/Server/StopLogDrain.php b/app/Actions/Server/StopLogDrain.php new file mode 100644 index 000000000..a5bce94a5 --- /dev/null +++ b/app/Actions/Server/StopLogDrain.php @@ -0,0 +1,20 @@ +server = Server::find(0); if (! $this->server) { return; @@ -48,7 +47,6 @@ class UpdateCoolify private function update() { if (isDev()) { - ray('Running in dev mode'); remote_process([ 'sleep 10', ], $this->server); diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index c7b1170a7..3dd91b4e2 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -19,18 +19,21 @@ class StopService ray('Stopping service: '.$service->name); $applications = $service->applications()->get(); foreach ($applications as $application) { - instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false); + instant_remote_process(command: ["docker stop --time=30 {$application->name}-{$service->uuid}"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false); $application->update(['status' => 'exited']); } $dbs = $service->databases()->get(); foreach ($dbs as $db) { - instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server, false); + instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false); + instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false); $db->update(['status' => 'exited']); } instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server); instant_remote_process(["docker network rm {$service->uuid}"], $service->server); } catch (\Exception $e) { - echo $e->getMessage(); ray($e->getMessage()); return $e->getMessage(); diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 1e177ca62..6f130626b 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -18,7 +18,12 @@ class CleanupDatabase extends Command } else { echo "Running database cleanup in dry-run mode...\n"; } - $keep_days = 60; + if (isCloud()) { + // Later on we can increase this to 180 days or dynamically set + $keep_days = 60; + } else { + $keep_days = 60; + } echo "Keep days: $keep_days\n"; // Cleanup failed jobs table $failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1)); diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 4a276cfc4..30d761a10 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Actions\Server\StopSentinel; +use App\Enums\ActivityTypes; use App\Enums\ApplicationDeploymentStatus; use App\Jobs\CleanupHelperContainersJob; use App\Models\ApplicationDeploymentQueue; @@ -12,22 +13,27 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; use Illuminate\Console\Command; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; class Init extends Command { - protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}'; + protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}'; protected $description = 'Cleanup instance related stuffs'; + public $servers = null; + public function handle() { + $this->servers = Server::all(); $this->alive(); get_public_ips(); if (version_compare('4.0.0-beta.312', config('version'), '<=')) { - $servers = Server::all(); - foreach ($servers as $server) { - $server->settings->update(['is_metrics_enabled' => false]); + foreach ($this->servers as $server) { + if ($server->settings->is_metrics_enabled === true) { + $server->settings->update(['is_metrics_enabled' => false]); + } if ($server->isFunctional()) { StopSentinel::dispatch($server); } @@ -36,7 +42,7 @@ class Init extends Command $full_cleanup = $this->option('full-cleanup'); $cleanup_deployments = $this->option('cleanup-deployments'); - + $cleanup_proxy_networks = $this->option('cleanup-proxy-networks'); $this->replace_slash_in_environment_name(); if ($cleanup_deployments) { echo "Running cleanup deployments.\n"; @@ -44,17 +50,26 @@ class Init extends Command return; } + if ($cleanup_proxy_networks) { + echo "Running cleanup proxy networks.\n"; + $this->cleanup_unused_network_from_coolify_proxy(); + + return; + } if ($full_cleanup) { // Required for falsely deleted coolify db $this->restore_coolify_db_backup(); + $this->update_traefik_labels(); + $this->cleanup_unused_network_from_coolify_proxy(); + $this->cleanup_unnecessary_dynamic_proxy_configuration(); $this->cleanup_in_progress_application_deployments(); $this->cleanup_stucked_helper_containers(); $this->call('cleanup:queue'); $this->call('cleanup:stucked-resources'); if (! isCloud()) { try { - $server = Server::find(0)->first(); - $server->setupDynamicProxyConfiguration(); + $localhost = $this->servers->where('id', 0)->first(); + $localhost->setupDynamicProxyConfiguration(); } catch (\Throwable $e) { echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; } @@ -68,6 +83,13 @@ class Init extends Command $settings->update(['is_auto_update_enabled' => false]); } } + if (isCloud()) { + $response = Http::retry(3, 1000)->get(config('constants.services.official')); + if ($response->successful()) { + $services = $response->json(); + File::put(base_path('templates/service-templates.json'), json_encode($services)); + } + } return; } @@ -75,6 +97,79 @@ class Init extends Command $this->call('cleanup:stucked-resources'); } + private function update_traefik_labels() + { + try { + Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']); + } catch (\Throwable $e) { + echo "Error in updating traefik labels: {$e->getMessage()}\n"; + } + } + + private function cleanup_unnecessary_dynamic_proxy_configuration() + { + if (isCloud()) { + foreach ($this->servers as $server) { + try { + if (! $server->isFunctional()) { + continue; + } + if ($server->id === 0) { + continue; + } + $file = $server->proxyPath().'/dynamic/coolify.yaml'; + + return instant_remote_process([ + "rm -f $file", + ], $server, false); + } catch (\Throwable $e) { + echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; + } + + } + } + } + + private function cleanup_unused_network_from_coolify_proxy() + { + foreach ($this->servers as $server) { + if (! $server->isFunctional()) { + continue; + } + if (! $server->isProxyShouldRun()) { + continue; + } + try { + ['networks' => $networks, 'allNetworks' => $allNetworks] = collectDockerNetworksByServer($server); + $removeNetworks = $allNetworks->diff($networks); + $commands = collect(); + foreach ($removeNetworks as $network) { + $out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false); + if (empty($out)) { + $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + } else { + $data = collect(json_decode($out, true)); + if ($data->count() === 1) { + // If only coolify-proxy itself is connected to that network (it should not be possible, but who knows) + $isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy'; + if ($isCoolifyProxyItself) { + $commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true"); + $commands->push("docker network rm $network >/dev/null 2>&1 || true"); + } + } + } + } + if ($commands->isNotEmpty()) { + echo "Cleaning up unused networks from coolify proxy\n"; + remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false); + } + } catch (\Throwable $e) { + echo "Error in cleaning up unused networks from coolify proxy: {$e->getMessage()}\n"; + } + } + } + private function restore_coolify_db_backup() { try { @@ -102,8 +197,7 @@ class Init extends Command private function cleanup_stucked_helper_containers() { - $servers = Server::all(); - foreach ($servers as $server) { + foreach ($this->servers as $server) { if ($server->isFunctional()) { CleanupHelperContainersJob::dispatch($server); } @@ -148,7 +242,6 @@ class Init extends Command private function cleanup_in_progress_application_deployments() { // Cleanup any failed deployments - try { if (isCloud()) { return; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index c6ee39524..e8f213b16 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,9 +2,8 @@ namespace App\Console; -use App\Jobs\CheckLogDrainContainerJob; +use App\Jobs\CheckForUpdatesJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\ContainerStatusJob; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; use App\Jobs\PullCoolifyImageJob; @@ -12,7 +11,9 @@ use App\Jobs\PullHelperImageJob; use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; -use App\Jobs\ServerStatusJob; +use App\Jobs\ServerCheckJob; +use App\Jobs\UpdateCoolifyJob; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledTask; use App\Models\Server; @@ -27,25 +28,26 @@ class Kernel extends ConsoleKernel protected function schedule(Schedule $schedule): void { $this->all_servers = Server::all(); + $settings = InstanceSettings::get(); + if (isDev()) { // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer(); + // Server Jobs $this->check_scheduled_backups($schedule); $this->check_resources($schedule); - $this->check_scheduled_backups($schedule); $this->check_scheduled_tasks($schedule); $schedule->command('uploads:clear')->everyTwoMinutes(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily(); - $schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer(); + $schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer(); + $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - // $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer(); + $this->schedule_updates($schedule); // Server Jobs $this->check_scheduled_backups($schedule); @@ -60,12 +62,26 @@ class Kernel extends ConsoleKernel private function pull_images($schedule) { + $settings = InstanceSettings::get(); $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { - $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); + $schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer(); } - $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); + $schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer(); + } + } + + private function schedule_updates($schedule) + { + $settings = InstanceSettings::get(); + + $updateCheckFrequency = $settings->update_check_frequency; + $schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer(); + + if ($settings->is_auto_update_enabled) { + $autoUpdateFrequency = $settings->auto_update_frequency; + $schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer(); } } @@ -75,19 +91,11 @@ class Kernel extends ConsoleKernel $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $own = Team::find(0)->servers; $servers = $servers->merge($own); - $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); } else { $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); - $containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false); - } - foreach ($containerServers as $server) { - $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); - if ($server->isLogDrainEnabled()) { - $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer(); - } } foreach ($servers as $server) { - $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer(); + $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer(); } } diff --git a/app/Enums/ProxyTypes.php b/app/Enums/ProxyTypes.php index 5ccf82e21..b3dc56746 100644 --- a/app/Enums/ProxyTypes.php +++ b/app/Enums/ProxyTypes.php @@ -5,7 +5,7 @@ namespace App\Enums; enum ProxyTypes: string { case NONE = 'NONE'; - case TRAEFIK_V2 = 'TRAEFIK_V2'; + case TRAEFIK = 'TRAEFIK'; case NGINX = 'NGINX'; case CADDY = 'CADDY'; } diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php new file mode 100644 index 000000000..27fdc6b5c --- /dev/null +++ b/app/Events/FileStorageChanged.php @@ -0,0 +1,32 @@ +teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 54ee8ef11..cd4d724b4 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -739,7 +739,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -835,7 +835,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -927,7 +927,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -947,7 +947,7 @@ class ApplicationsController extends Controller ])); } elseif ($type === 'dockerfile') { if (! $request->has('name')) { - $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + $request->offsetSet('name', 'dockerfile-'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1009,7 +1009,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -1025,7 +1025,7 @@ class ApplicationsController extends Controller ])); } elseif ($type === 'dockerimage') { if (! $request->has('name')) { - $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + $request->offsetSet('name', 'docker-image-'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1067,7 +1067,7 @@ class ApplicationsController extends Controller $application->isConfigurationChanged(true); if ($instantDeploy) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -1099,7 +1099,7 @@ class ApplicationsController extends Controller ], 422); } if (! $request->has('name')) { - $request->offsetSet('name', 'service'.new Cuid2(7)); + $request->offsetSet('name', 'service'.new Cuid2); } $validator = customApiValidator($request->all(), [ sharedDataApplications(), @@ -1320,7 +1320,7 @@ class ApplicationsController extends Controller #[OA\Patch( summary: 'Update', description: 'Update application by UUID.', - path: '/applications', + path: '/applications/{uuid}', security: [ ['bearerAuth' => []], ], @@ -2322,7 +2322,7 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -2479,7 +2479,7 @@ class ApplicationsController extends Controller return response()->json(['message' => 'Application not found.'], 404); } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index 2ee56d0cd..437162058 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -84,7 +84,7 @@ class DeployController extends Controller ], tags: ['Deployments'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -290,7 +290,7 @@ class DeployController extends Controller } switch ($resource?->getMorphClass()) { case 'App\Models\Application': - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $resource, deployment_uuid: $deployment_uuid, diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php index 59731ef40..60337a76c 100644 --- a/app/Http/Controllers/Api/OpenApi.php +++ b/app/Http/Controllers/Api/OpenApi.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api; use OpenApi\Attributes as OA; #[OA\Info(title: 'Coolify', version: '0.1')] -#[OA\Server(url: 'https://app.coolify.io/api/v1')] +#[OA\Server(url: 'https://app.coolify.io/api/v1', description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.')] #[OA\SecurityScheme( type: 'http', scheme: 'bearer', diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index 8316d5e82..6aec31e9b 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -61,7 +61,7 @@ class ProjectController extends Controller ], tags: ['Projects'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -107,7 +107,7 @@ class ProjectController extends Controller ], tags: ['Projects'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')), ], responses: [ diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index 11e8e27ca..67128234e 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -73,7 +73,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -318,7 +318,7 @@ class SecurityController extends Controller ], tags: ['Private Keys'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index fa159077d..9044c4a35 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -105,7 +105,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -182,7 +182,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -259,7 +259,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( @@ -525,7 +525,7 @@ class ServersController extends Controller 'private_key_id' => $privateKey->id, 'team_id' => $teamId, 'proxy' => [ - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ], ]); @@ -732,7 +732,7 @@ class ServersController extends Controller ], tags: ['Servers'], parameters: [ - new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')), ], responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 7d58987d8..3a5b59f55 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -413,6 +413,8 @@ class ServicesController extends Controller return response()->json(['message' => 'Service not found.'], 404); } + $service = $service->load(['applications', 'databases']); + return response()->json($this->removeSensitiveData($service)); } diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index 059438ff4..ef85d59e3 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -103,7 +103,7 @@ class Bitbucket extends Controller if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -127,7 +127,7 @@ class Bitbucket extends Controller if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index e6d91efd6..e042b74c9 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -123,7 +123,7 @@ class Gitea extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -162,7 +162,7 @@ class Gitea extends Controller if ($x_gitea_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index ee51b6e0d..5f3ba933b 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -128,7 +128,7 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -167,7 +167,7 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { @@ -357,7 +357,7 @@ class Github extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -396,7 +396,7 @@ class Github extends Controller if ($x_github_event === 'pull_request') { if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { ApplicationPreview::create([ diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index f6e6cf7e7..ec7f51a0d 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -137,7 +137,7 @@ class Gitlab extends Controller $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { ray('Deploying '.$application->name.' with branch '.$branch); - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, deployment_uuid: $deployment_uuid, @@ -177,7 +177,7 @@ class Gitlab extends Controller if ($x_gitlab_event === 'merge_request') { if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') { if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { if ($application->build_pack === 'dockercompose') { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 8a79515b5..473cbc679 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -307,14 +307,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue ] ); - // $this->execute_remote_command( - // [ - // "docker image prune -f >/dev/null 2>&1", - // "hidden" => true, - // "ignore_errors" => true, - // ] - // ); - ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); } } @@ -497,13 +489,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } else { $this->write_deployment_configurations(); $server_workdir = $this->application->workdir(); + $this->docker_compose_location = '/docker-compose.yaml'; $command = "{$this->coolify_variables} docker compose"; if ($this->env_filename) { - $command .= " --env-file {$this->workdir}/{$this->env_filename}"; + $command .= " --env-file {$server_workdir}/{$this->env_filename}"; } $command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d"; - $this->execute_remote_command( ['command' => $command, 'hidden' => true], ); @@ -636,21 +628,26 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->server = $this->original_server; } $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + + $mainDir = $this->configuration_dir; + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $mainDir = $this->application->workdir(); + } if ($this->pull_request_id === 0) { - $composeFileName = "$this->configuration_dir/docker-compose.yaml"; + $composeFileName = "$mainDir/docker-compose.yaml"; } else { - $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yaml"; + $composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml"; $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; } $this->execute_remote_command( [ - "mkdir -p $this->configuration_dir", + "mkdir -p $mainDir", ], [ "echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null", ], [ - "echo '{$readme}' > $this->configuration_dir/README.md", + "echo '{$readme}' > $mainDir/README.md", ] ); if ($this->use_build_server) { @@ -874,8 +871,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default - if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } } // Add HOST if not exists if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) { @@ -918,15 +917,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $envs->push($env->key.'='.$real_value); } // Add PORT if not exists, use the first port as default - if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { - $envs->push("PORT={$ports[0]}"); + if ($this->build_pack !== 'dockercompose') { + if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) { + $envs->push("PORT={$ports[0]}"); + } } // Add HOST if not exists if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) { $envs->push('HOST=0.0.0.0'); } } - if ($envs->isEmpty()) { $this->env_filename = null; if ($this->use_build_server) { @@ -1025,7 +1025,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue $this->write_deployment_configurations(); $this->server = $this->original_server; } - if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { + if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { $this->application_deployment_queue->addLogEntry('----------------------------------------'); if (count($this->application->ports_mappings_array) > 0) { $this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.'); @@ -2027,27 +2027,43 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->addLogEntry('Building docker image completed.'); } + /** + * @param int $timeout in seconds + */ + private function graceful_shutdown_container(string $containerName, int $timeout = 30) + { + try { + $this->execute_remote_command( + ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true], + ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + } catch (\Exception $error) { + // report error if needed + } + + $this->execute_remote_command( + ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true] + ); + + } + private function stop_running_container(bool $force = false) { $this->application_deployment_queue->addLogEntry('Removing old containers.'); if ($this->newVersionIsHealthy || $force) { - $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); - if ($this->pull_request_id === 0) { - $containers = $containers->filter(function ($container) { - return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { + $this->graceful_shutdown_container($this->container_name); + } else { + $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); + if ($this->pull_request_id === 0) { + $containers = $containers->filter(function ($container) { + return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id; + }); + } + $containers->each(function ($container) { + $this->graceful_shutdown_container(data_get($container, 'Names')); }); } - $containers->each(function ($container) { - $containerName = data_get($container, 'Names'); - $this->execute_remote_command( - ["docker rm -f $containerName >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); - }); - if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); - } } else { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('----------------------------------------'); @@ -2058,9 +2074,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::FAILED->value, ]); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true], - ); + $this->graceful_shutdown_container($this->container_name); } } @@ -2235,7 +2249,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one - if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { + if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { // do not remove already running container } else { $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php new file mode 100644 index 000000000..86b66fbfb --- /dev/null +++ b/app/Jobs/CheckForUpdatesJob.php @@ -0,0 +1,42 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $latest_version = data_get($versions, 'coolify.v4.version'); + $current_version = config('version'); + + if (version_compare($latest_version, $current_version, '>')) { + // New version available + $settings->update(['new_version_available' => true]); + } else { + $settings->update(['new_version_available' => false]); + } + } + } catch (\Throwable $e) { + // Consider implementing a notification to administrators + } + } +} diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 4afe50d53..79b00e9cd 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -90,6 +90,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { try { BackupCreated::dispatch($this->team->id); + // Check if team is exists if (is_null($this->team)) { $this->backup->update(['status' => 'failed']); @@ -476,7 +477,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue } else { $network = $this->database->destination->network; } - $commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; + $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; instant_remote_process($commands, $this->server); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index f99a65b5d..5010263ae 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -26,17 +26,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public function handle(): void { try { - // $isInprogress = false; - // $this->server->applications()->each(function ($application) use (&$isInprogress) { - // if ($application->isDeploymentInprogress()) { - // $isInprogress = true; - - // return; - // } - // }); - // if ($isInprogress) { - // throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); - // } if (! $this->server->isFunctional()) { return; } @@ -48,6 +37,12 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue } $this->usageBefore = $this->server->getDiskUsage(); + if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { + Log::info('DockerCleanupJob force cleanup on '.$this->server->name); + CleanupDocker::run(server: $this->server, force: true); + + return; + } if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { CleanupDocker::run(server: $this->server, force: false); $usageAfter = $this->server->getDiskUsage(); diff --git a/app/Jobs/PullCoolifyImageJob.php b/app/Jobs/PullCoolifyImageJob.php index 253b0b9f0..f0912493f 100644 --- a/app/Jobs/PullCoolifyImageJob.php +++ b/app/Jobs/PullCoolifyImageJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\InstanceSettings; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -16,16 +17,13 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $timeout = 1000; - - public function __construct() {} - public function handle(): void { try { if (isDev() || isCloud()) { return; } + $settings = InstanceSettings::get(); $server = Server::findOrFail(0); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); if ($response->successful()) { @@ -35,7 +33,6 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue $latest_version = get_latest_version_of_coolify(); instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false); - $settings = \App\Models\InstanceSettings::get(); $current_version = config('version'); if (! $settings->is_auto_update_enabled) { return; @@ -46,10 +43,6 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue if (version_compare($latest_version, $current_version, '<')) { return; } - instant_remote_process([ - 'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh', - "bash /data/coolify/source/upgrade.sh $latest_version", - ], $server); } catch (\Throwable $e) { throw $e; } diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 396ff29f4..72c971033 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -22,15 +22,16 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue public function handle(): void { try { - if (! isDev()) { - ray('PullTemplatesAndVersions service-templates'); - $response = Http::retry(3, 1000)->get(config('constants.services.official')); - if ($response->successful()) { - $services = $response->json(); - File::put(base_path('templates/service-templates.json'), json_encode($services)); - } else { - send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); - } + if (isDev() || isCloud()) { + return; + } + ray('PullTemplatesAndVersions service-templates'); + $response = Http::retry(3, 1000)->get(config('constants.services.official')); + if ($response->successful()) { + $services = $response->json(); + File::put(base_path('templates/service-templates.json'), json_encode($services)); + } else { + send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); } } catch (\Throwable $e) { send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage()); diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php new file mode 100644 index 000000000..3976477e7 --- /dev/null +++ b/app/Jobs/ServerCheckJob.php @@ -0,0 +1,440 @@ +server->uuid))]; + } + + public function uniqueId(): int + { + return $this->server->uuid; + } + + public function handle() + { + try { + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->services = $this->server->services()->get(); + $this->previews = $this->server->previews(); + + $up = $this->serverStatus(); + if (! $up) { + ray('Server is not reachable.'); + + return 'Server is not reachable.'; + } + if (! $this->server->isFunctional()) { + ray('Server is not ready.'); + + return 'Server is not ready.'; + } + if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { + ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); + if (is_null($this->containers)) { + return 'No containers found.'; + } + GetContainersStatus::run($this->server, $this->containers, $containerReplicates); + $this->checkLogDrainContainer(); + $this->checkSentinel(); + } + + } catch (\Throwable $e) { + ray($e->getMessage()); + + return handleError($e); + } + + } + + private function checkSentinel() + { + if ($this->server->isSentinelEnabled()) { + $sentinelContainerFound = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-sentinel'; + })->first(); + if ($sentinelContainerFound) { + $status = data_get($sentinelContainerFound, 'State.Status'); + if ($status !== 'running') { + PullSentinelImageJob::dispatch($this); + } + } + } + } + + private function serverStatus() + { + ['uptime' => $uptime] = $this->server->validateConnection(); + if ($uptime) { + if ($this->server->unreachable_notification_sent === true) { + $this->server->update(['unreachable_notification_sent' => false]); + } + } else { + // $this->server->team?->notify(new Unreachable($this->server)); + foreach ($this->applications as $application) { + $application->update(['status' => 'exited']); + } + foreach ($this->databases as $database) { + $database->update(['status' => 'exited']); + } + foreach ($this->services as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->update(['status' => 'exited']); + } + foreach ($dbs as $db) { + $db->update(['status' => 'exited']); + } + } + + return false; + } + + return true; + + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + InstallLogDrain::dispatch($this->server); + } + } else { + InstallLogDrain::dispatch($this->server); + } + } + + private function containerStatus() + { + + $foundApplications = []; + $foundApplicationPreviews = []; + $foundDatabases = []; + $foundServices = []; + + foreach ($this->containers as $container) { + if ($this->server->isSwarm()) { + $labels = data_get($container, 'Spec.Labels'); + $uuid = data_get($labels, 'coolify.name'); + } else { + $labels = data_get($container, 'Config.Labels'); + } + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = Arr::undot(format_docker_labels_to_json($labels)); + $applicationId = data_get($labels, 'coolify.applicationId'); + if ($applicationId) { + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + if ($pullRequestId) { + if (str($applicationId)->contains('-')) { + $applicationId = str($applicationId)->before('-'); + } + $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + if ($preview) { + $foundApplicationPreviews[] = $preview->id; + $statusFromDb = $preview->status; + if ($statusFromDb !== $containerStatus) { + $preview->update(['status' => $containerStatus]); + } + } else { + //Notify user that this container should not be there. + } + } else { + $application = $this->applications->where('id', $applicationId)->first(); + if ($application) { + $foundApplications[] = $application->id; + $statusFromDb = $application->status; + if ($statusFromDb !== $containerStatus) { + $application->update(['status' => $containerStatus]); + } + } else { + //Notify user that this container should not be there. + } + } + } else { + $uuid = data_get($labels, 'com.docker.compose.service'); + $type = data_get($labels, 'coolify.type'); + + if ($uuid) { + if ($type === 'service') { + $database_id = data_get($labels, 'coolify.service.subId'); + if ($database_id) { + $service_db = ServiceDatabase::where('id', $database_id)->first(); + if ($service_db) { + $uuid = data_get($service_db, 'service.uuid'); + if ($uuid) { + $isPublic = data_get($service_db, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($service_db); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); + } + } + } + } + } + } else { + $database = $this->databases->where('uuid', $uuid)->first(); + if ($database) { + $isPublic = data_get($database, 'is_public'); + $foundDatabases[] = $database->id; + $statusFromDb = $database->status; + if ($statusFromDb !== $containerStatus) { + $database->update(['status' => $containerStatus]); + } + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } + } + } else { + // Notify user that this container should not be there. + } + } + } + if (data_get($container, 'Name') === '/coolify-db') { + $foundDatabases[] = 0; + } + } + $serviceLabelId = data_get($labels, 'coolify.serviceId'); + if ($serviceLabelId) { + $subType = data_get($labels, 'coolify.service.subType'); + $subId = data_get($labels, 'coolify.service.subId'); + $service = $this->services->where('id', $serviceLabelId)->first(); + if (! $service) { + continue; + } + if ($subType === 'application') { + $service = $service->applications()->where('id', $subId)->first(); + } else { + $service = $service->databases()->where('id', $subId)->first(); + } + if ($service) { + $foundServices[] = "$service->id-$service->name"; + $statusFromDb = $service->status; + if ($statusFromDb !== $containerStatus) { + // ray('Updating status: ' . $containerStatus); + $service->update(['status' => $containerStatus]); + } + } + } + } + $exitedServices = collect([]); + foreach ($this->services as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + if (in_array("$app->id-$app->name", $foundServices)) { + continue; + } else { + $exitedServices->push($app); + } + } + foreach ($dbs as $db) { + if (in_array("$db->id-$db->name", $foundServices)) { + continue; + } else { + $exitedServices->push($db); + } + } + } + $exitedServices = $exitedServices->unique('id'); + foreach ($exitedServices as $exitedService) { + if (str($exitedService->status)->startsWith('exited')) { + continue; + } + $name = data_get($exitedService, 'name'); + $fqdn = data_get($exitedService, 'fqdn'); + if ($name) { + if ($fqdn) { + $containerName = "$name, available at $fqdn"; + } else { + $containerName = $name; + } + } else { + if ($fqdn) { + $containerName = $fqdn; + } else { + $containerName = null; + } + } + $projectUuid = data_get($service, 'environment.project.uuid'); + $serviceUuid = data_get($service, 'uuid'); + $environmentName = data_get($service, 'environment.name'); + + if ($projectUuid && $serviceUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; + } else { + $url = null; + } + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + $exitedService->update(['status' => 'exited']); + } + + $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); + foreach ($notRunningApplications as $applicationId) { + $application = $this->applications->where('id', $applicationId)->first(); + if (str($application->status)->startsWith('exited')) { + continue; + } + $application->update(['status' => 'exited']); + + $name = data_get($application, 'name'); + $fqdn = data_get($application, 'fqdn'); + + $containerName = $name ? "$name ($fqdn)" : $fqdn; + + $projectUuid = data_get($application, 'environment.project.uuid'); + $applicationUuid = data_get($application, 'uuid'); + $environment = data_get($application, 'environment.name'); + + if ($projectUuid && $applicationUuid && $environment) { + $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; + } else { + $url = null; + } + + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + $notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews); + foreach ($notRunningApplicationPreviews as $previewId) { + $preview = $this->previews->where('id', $previewId)->first(); + if (str($preview->status)->startsWith('exited')) { + continue; + } + $preview->update(['status' => 'exited']); + + $name = data_get($preview, 'name'); + $fqdn = data_get($preview, 'fqdn'); + + $containerName = $name ? "$name ($fqdn)" : $fqdn; + + $projectUuid = data_get($preview, 'application.environment.project.uuid'); + $environmentName = data_get($preview, 'application.environment.name'); + $applicationUuid = data_get($preview, 'application.uuid'); + + if ($projectUuid && $applicationUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; + } else { + $url = null; + } + + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + $notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases); + foreach ($notRunningDatabases as $database) { + $database = $this->databases->where('id', $database)->first(); + if (str($database->status)->startsWith('exited')) { + continue; + } + $database->update(['status' => 'exited']); + + $name = data_get($database, 'name'); + $fqdn = data_get($database, 'fqdn'); + + $containerName = $name; + + $projectUuid = data_get($database, 'environment.project.uuid'); + $environmentName = data_get($database, 'environment.name'); + $databaseUuid = data_get($database, 'uuid'); + + if ($projectUuid && $databaseUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; + } else { + $url = null; + } + // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); + } + + // Check if proxy is running + $this->server->proxyType(); + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + ray($e); + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } +} diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php new file mode 100644 index 000000000..4c65a711f --- /dev/null +++ b/app/Jobs/UpdateCoolifyJob.php @@ -0,0 +1,51 @@ +new_version_available) { + Log::info('No new version available. Skipping update.'); + + return; + } + + $server = Server::findOrFail(0); + if (! $server) { + Log::error('Server not found. Cannot proceed with update.'); + + return; + } + + Log::info('Starting Coolify update process...'); + UpdateCoolify::run(false); // false means it's not a manual update + + $settings->update(['new_version_available' => false]); + Log::info('Coolify update completed successfully.'); + + } catch (\Throwable $e) { + Log::error('UpdateCoolifyJob failed: '.$e->getMessage()); + // Consider implementing a notification to administrators + } + } +} diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 8127ca009..147a1ad6f 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -179,7 +179,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== public function getProxyType() { // Set Default Proxy Type - $this->selectProxy(ProxyTypes::TRAEFIK_V2->value); + $this->selectProxy(ProxyTypes::TRAEFIK->value); // $proxyTypeSet = $this->createdServer->proxy->type; // if (!$proxyTypeSet) { // $this->currentState = 'select-proxy'; diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index f822cfa5f..4fc938df8 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -52,7 +52,7 @@ class Docker extends Component if (request()->query('network_name')) { $this->network = request()->query('network_name'); } else { - $this->network = new Cuid2(7); + $this->network = new Cuid2; } if ($this->servers->count() > 0) { $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab(); diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php index 156c63d3a..42d276e64 100644 --- a/app/Livewire/MonacoEditor.php +++ b/app/Livewire/MonacoEditor.php @@ -39,7 +39,7 @@ class MonacoEditor extends Component public function render() { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php new file mode 100644 index 000000000..ec196c154 --- /dev/null +++ b/app/Livewire/NavbarDeleteTeam.php @@ -0,0 +1,35 @@ +delete(); + + $currentTeam->members->each(function ($user) use ($currentTeam) { + if ($user->id === auth()->user()->id) { + return; + } + $user->teams()->detach($currentTeam); + $session = DB::table('sessions')->where('user_id', $user->id)->first(); + if ($session) { + DB::table('sessions')->where('id', $session->id)->delete(); + } + }); + + refreshSession(); + + return redirect()->route('team.index'); + } + + public function render() + { + return view('livewire.navbar-delete-team'); + } +} diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index 3b402b3ec..cde3322db 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -91,11 +91,25 @@ class Advanced extends Component public function saveCustomName() { - if (isset($this->application->settings->custom_internal_name)) { + if (str($this->application->settings->custom_internal_name)->isNotEmpty()) { $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value(); } else { $this->application->settings->custom_internal_name = null; } + $customInternalName = $this->application->settings->custom_internal_name; + $server = $this->application->destination->server; + $allApplications = $server->applications(); + + $foundSameInternalName = $allApplications->filter(function ($application) { + return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->application->settings->custom_internal_name; + }); + if ($foundSameInternalName->isNotEmpty()) { + $this->dispatch('error', 'This custom container name is already in use by another application on this server.'); + $this->application->settings->custom_internal_name = $customInternalName; + $this->application->settings->refresh(); + + return; + } $this->application->settings->save(); $this->dispatch('success', 'Custom name saved.'); } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 7dfd9bad4..77593bf0a 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -214,7 +214,7 @@ class General extends Component } $this->dispatch('success', 'Docker compose file loaded.'); $this->dispatch('compose_loaded'); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); $this->dispatch('refreshEnvs'); } catch (\Throwable $e) { $this->application->docker_compose_location = $this->initialDockerComposeLocation; @@ -228,7 +228,7 @@ class General extends Component public function generateDomain(string $serviceName) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; $domain = generateFqdn($this->application->destination->server, $uuid); $this->parsedServiceDomains[$serviceName]['domain'] = $domain; $this->application->docker_compose_domains = json_encode($this->parsedServiceDomains); diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index feb54c7f0..c02949e17 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -5,8 +5,6 @@ namespace App\Livewire\Project\Application; use App\Actions\Application\StopApplication; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ContainerStatusJob; -use App\Jobs\ServerStatusJob; use App\Models\Application; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -46,11 +44,7 @@ class Heading extends Component { if ($this->application->destination->server->isFunctional()) { GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); - // dispatch(new ContainerStatusJob($this->application->destination->server)); - } else { - dispatch(new ServerStatusJob($this->application->destination->server)); } - if ($showNotification) { $this->dispatch('success', 'Success', 'Application status updated.'); } @@ -102,7 +96,7 @@ class Heading extends Component protected function setDeploymentUuid() { - $this->deploymentUuid = new Cuid2(7); + $this->deploymentUuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deploymentUuid; } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index df64c3fd3..30bc0a9d1 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -85,7 +85,7 @@ class Previews extends Component $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn); @@ -170,7 +170,7 @@ class Previews extends Component protected function setDeploymentUuid() { - $this->deployment_uuid = new Cuid2(7); + $this->deployment_uuid = new Cuid2; $this->parameters['deployment_uuid'] = $this->deployment_uuid; } diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php index bf4478e53..b3e838bb3 100644 --- a/app/Livewire/Project/Application/PreviewsCompose.php +++ b/app/Livewire/Project/Application/PreviewsCompose.php @@ -44,7 +44,7 @@ class PreviewsCompose extends Component $template = $this->preview->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn); diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index ed0ac1cef..1e58a1458 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -23,7 +23,7 @@ class Rollback extends Component public function rollbackImage($commit) { - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; queue_application_deployment( application: $this->application, diff --git a/app/Livewire/Project/CloneMe.php b/app/Livewire/Project/CloneMe.php index 5373f1b3f..4d2bc6589 100644 --- a/app/Livewire/Project/CloneMe.php +++ b/app/Livewire/Project/CloneMe.php @@ -47,7 +47,7 @@ class CloneMe extends Component $this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->project_id = $this->project->id; $this->servers = currentTeam()->servers; - $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2(7))->slug(); + $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); } public function render() @@ -106,7 +106,7 @@ class CloneMe extends Component $databases = $this->environment->databases(); $services = $this->environment->services; foreach ($applications as $application) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newApplication = $application->replicate()->fill([ 'uuid' => $uuid, 'fqdn' => generateFqdn($this->server, $uuid), @@ -133,7 +133,7 @@ class CloneMe extends Component } } foreach ($databases as $database) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newDatabase = $database->replicate()->fill([ 'uuid' => $uuid, 'status' => 'exited', @@ -161,7 +161,7 @@ class CloneMe extends Component } } foreach ($services as $service) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $newService = $service->replicate()->fill([ 'uuid' => $uuid, 'environment_id' => $environment->id, diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index fdad052c7..d3f5b5261 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -48,7 +48,7 @@ class DockerImage extends Component $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); ray($image, $tag); $application = Application::create([ - 'name' => 'docker-image-'.new Cuid2(7), + 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php index 6f6bc9185..3c7f42329 100644 --- a/app/Livewire/Project/New/SimpleDockerfile.php +++ b/app/Livewire/Project/New/SimpleDockerfile.php @@ -53,7 +53,7 @@ CMD ["nginx", "-g", "daemon off;"] $port = 80; } $application = Application::create([ - 'name' => 'dockerfile-'.new Cuid2(7), + 'name' => 'dockerfile-'.new Cuid2, 'repository_project_id' => 0, 'git_repository' => 'coollabsio/coolify', 'git_branch' => 'main', diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index 47534ded1..c82012aaa 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -25,7 +25,6 @@ class Configuration extends Component return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', 'check_status', - 'refresh' => '$refresh', ]; } @@ -76,8 +75,7 @@ class Configuration extends Component { try { GetContainersStatus::run($this->service->server); - // dispatch_sync(new ContainerStatusJob($this->service->server)); - $this->dispatch('refresh')->self(); + $this->dispatch('$refresh'); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2eea0891f..2d9c95daa 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -26,6 +26,8 @@ class FileStorage extends Component public ?string $workdir = null; + public bool $permanently_delete = true; + protected $rules = [ 'fileStorage.is_directory' => 'required', 'fileStorage.fs_path' => 'required', @@ -56,7 +58,7 @@ class FileStorage extends Component } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } @@ -71,20 +73,27 @@ class FileStorage extends Component } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } public function delete() { try { - $this->fileStorage->deleteStorageOnServer(); + $message = 'File deleted.'; + if ($this->fileStorage->is_directory) { + $message = 'Directory deleted.'; + } + if ($this->permanently_delete) { + $message = 'Directory deleted from the server.'; + $this->fileStorage->deleteStorageOnServer(); + } $this->fileStorage->delete(); - $this->dispatch('success', 'File deleted.'); + $this->dispatch('success', $message); } catch (\Throwable $e) { return handleError($e, $this); } finally { - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index f2fd5ae96..674182df5 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -49,6 +49,11 @@ class Navbar extends Component } } + public function check_status_without_notification() + { + $this->dispatch('check_status'); + } + public function check_status() { $this->dispatch('check_status'); @@ -63,6 +68,8 @@ class Navbar extends Component public function checkDeployments() { try { + // TODO: This is a temporary solution. We need to refactor this. + // We need to delete null bytes somehow. $activity = Activity::where('properties->type_uuid', $this->service->uuid)->latest()->first(); $status = data_get($activity, 'properties.status'); if ($status === 'queued' || $status === 'in_progress') { @@ -70,7 +77,7 @@ class Navbar extends Component } else { $this->isDeploymentProgress = false; } - } catch (\Exception $e) { + } catch (\Throwable $e) { $this->isDeploymentProgress = false; } } diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 161c38097..4b64a8b5e 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -9,14 +9,36 @@ class Storage extends Component { public $resource; + public $fileStorage; + public function getListeners() { + $teamId = auth()->user()->currentTeam()->id; + return [ + "echo-private:team.{$teamId},FileStorageChanged" => 'refreshStoragesFromEvent', + 'refreshStorages', 'addNewVolume', - 'refresh_storages' => '$refresh', ]; } + public function mount() + { + $this->refreshStorages(); + } + + public function refreshStoragesFromEvent() + { + $this->refreshStorages(); + $this->dispatch('warning', 'File storage changed. Usually it means that the file / directory is already defined on the server, so Coolify set it up for you properly on the UI.'); + } + + public function refreshStorages() + { + $this->fileStorage = $this->resource->fileStorages()->get(); + $this->dispatch('$refresh'); + } + public function addNewVolume($data) { try { @@ -30,7 +52,7 @@ class Storage extends Component $this->resource->refresh(); $this->dispatch('success', 'Storage added successfully'); $this->dispatch('clearAddStorage'); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index 30c35410f..5f0178be4 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -22,7 +22,7 @@ class Danger extends Component public function mount() { - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $parameters = get_route_parameters(); $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentName = data_get($parameters, 'environment_name'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 22ada8ab8..a2c018beb 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -67,7 +67,7 @@ class Destination extends Component return; } - $deployment_uuid = new Cuid2(7); + $deployment_uuid = new Cuid2; $server = Server::find($server_id); $destination = StandaloneDocker::find($network_id); queue_application_deployment( diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php index b732b6b52..a859c90b0 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php @@ -48,14 +48,14 @@ class Add extends Component public function submit() { $this->validate(); - if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { - $type = str($this->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) { + // $type = str($this->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $this->dispatch('saveKey', [ 'key' => $this->key, 'value' => $this->value, diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index d1edaf4f5..9e6760293 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -39,7 +39,7 @@ class All extends Component if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->sortMe(); $this->getDevView(); } @@ -125,14 +125,14 @@ class All extends Component continue; } $found->value = $variable; - if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { - $type = str($found->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($found->value)->startsWith('{{') && str($found->value)->endsWith('}}')) { + // $type = str($found->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $found->save(); continue; @@ -140,14 +140,14 @@ class All extends Component $environment = new EnvironmentVariable; $environment->key = $key; $environment->value = $variable; - if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { - $type = str($environment->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($environment->value)->startsWith('{{') && str($environment->value)->endsWith('}}')) { + // $type = str($environment->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $environment->is_build_time = false; $environment->is_multiline = false; $environment->is_preview = $isPreview ? true : false; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index d78b47363..e63871602 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -58,7 +58,7 @@ class Show extends Component if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { $this->isSharedVariable = true; } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->parameters = get_route_parameters(); $this->checkEnvs(); } @@ -108,14 +108,14 @@ class Show extends Component } else { $this->validate(); } - if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { - $type = str($this->env->value)->after('{{')->before('.')->value; - if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); + // if (str($this->env->value)->startsWith('{{') && str($this->env->value)->endsWith('}}')) { + // $type = str($this->env->value)->after('{{')->before('.')->value; + // if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { + // $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.'); - return; - } - } + // return; + // } + // } $this->serialize(); $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 586a125ae..ec09eb80f 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -39,7 +39,7 @@ class ResourceOperations extends Component if (! $new_destination) { return $this->addError('destination_id', 'Destination not found.'); } - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $server = $new_destination->server; if ($this->resource->getMorphClass() === 'App\Models\Application') { $new_resource = $this->resource->replicate()->fill([ @@ -87,7 +87,7 @@ class ResourceOperations extends Component $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' ) { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -121,7 +121,7 @@ class ResourceOperations extends Component return redirect()->to($route); } elseif ($this->resource->type() === 'service') { - $uuid = (string) new Cuid2(7); + $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index dbd420d94..8be4ff643 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -47,7 +47,7 @@ class Show extends Component $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); } - $this->modalId = new Cuid2(7); + $this->modalId = new Cuid2; $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); } diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index d22f3b05f..27e0c6e44 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -54,7 +54,11 @@ class Add extends Component public function mount() { - $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}"; + if (str($this->resource->getMorphClass())->contains('Standalone')) { + $this->file_storage_directory_source = database_configuration_dir()."/{$this->resource->uuid}"; + } else { + $this->file_storage_directory_source = application_configuration_dir()."/{$this->resource->uuid}"; + } $this->uuid = $this->resource->uuid; $this->parameters = get_route_parameters(); if (data_get($this->parameters, 'application_uuid')) { @@ -92,7 +96,7 @@ class Add extends Component 'resource_type' => get_class($this->resource), ], ); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -119,7 +123,7 @@ class Add extends Component 'resource_type' => get_class($this->resource), ], ); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Storages/All.php b/app/Livewire/Project/Shared/Storages/All.php index d2014694e..c26315d3b 100644 --- a/app/Livewire/Project/Shared/Storages/All.php +++ b/app/Livewire/Project/Shared/Storages/All.php @@ -8,5 +8,5 @@ class All extends Component { public $resource; - protected $listeners = ['refresh_storages' => '$refresh']; + protected $listeners = ['refreshStorages' => '$refresh']; } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index b64cbfa11..08f51ce08 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -39,6 +39,6 @@ class Show extends Component public function delete() { $this->storage->delete(); - $this->dispatch('refresh_storages'); + $this->dispatch('refreshStorages'); } } diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index 3d7b34de1..6e09eecdd 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -3,6 +3,7 @@ namespace App\Livewire\Server; use App\Actions\Server\InstallLogDrain; +use App\Actions\Server\StopLogDrain; use App\Models\Server; use Livewire\Component; @@ -132,6 +133,9 @@ class LogDrains extends Component 'is_logdrain_axiom_enabled' => false, ]); } + if (! $this->server->isLogDrainEnabled()) { + StopLogDrain::dispatch($this->server); + } $this->server->settings->save(); $this->dispatch('success', 'Settings saved.'); diff --git a/app/Livewire/Server/New/ByIp.php b/app/Livewire/Server/New/ByIp.php index 0f4c1afea..5f69835d7 100644 --- a/app/Livewire/Server/New/ByIp.php +++ b/app/Livewire/Server/New/ByIp.php @@ -104,7 +104,7 @@ class ByIp extends Component 'private_key_id' => $this->private_key_id, 'proxy' => [ // set default proxy type to traefik v2 - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ], ]; diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index a170ee029..123b29d70 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -20,6 +20,10 @@ class Proxy extends Component protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit']; + protected $rules = [ + 'server.settings.generate_exact_labels' => 'required|boolean', + ]; + public function mount() { $this->selectedProxy = $this->server->proxyType(); @@ -31,13 +35,13 @@ class Proxy extends Component $this->dispatch('refresh')->self(); } - public function change_proxy() + public function changeProxy() { $this->server->proxy = null; $this->server->save(); } - public function select_proxy($proxy_type) + public function selectProxy($proxy_type) { $this->server->proxy->set('status', 'exited'); $this->server->proxy->set('type', $proxy_type); @@ -49,6 +53,17 @@ class Proxy extends Component $this->dispatch('proxyStatusUpdated'); } + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->save(); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function submit() { try { diff --git a/app/Livewire/Server/Proxy/DynamicConfigurations.php b/app/Livewire/Server/Proxy/DynamicConfigurations.php index c858481db..6277a24bd 100644 --- a/app/Livewire/Server/Proxy/DynamicConfigurations.php +++ b/app/Livewire/Server/Proxy/DynamicConfigurations.php @@ -21,7 +21,6 @@ class DynamicConfigurations extends Component return [ "echo-private:team.{$teamId},ProxyStatusChanged" => 'loadDynamicConfigurations', 'loadDynamicConfigurations', - 'refresh' => '$refresh', ]; } @@ -42,7 +41,7 @@ class DynamicConfigurations extends Component $contents[$without_extension] = instant_remote_process(["cat {$proxy_path}/dynamic/{$file}"], $this->server); } $this->contents = $contents; - $this->dispatch('refresh'); + $this->dispatch('$refresh'); } public function mount() diff --git a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php index e5de6eda0..2155f1e82 100644 --- a/app/Livewire/Server/Proxy/NewDynamicConfiguration.php +++ b/app/Livewire/Server/Proxy/NewDynamicConfiguration.php @@ -2,6 +2,7 @@ namespace App\Livewire\Server\Proxy; +use App\Enums\ProxyTypes; use App\Models\Server; use Livewire\Component; use Symfony\Component\Yaml\Yaml; @@ -45,7 +46,7 @@ class NewDynamicConfiguration extends Component return redirect()->route('server.index'); } $proxy_type = $this->server->proxyType(); - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { if (! str($this->fileName)->endsWith('.yaml') && ! str($this->fileName)->endsWith('.yml')) { $this->fileName = "{$this->fileName}.yaml"; } @@ -69,7 +70,7 @@ class NewDynamicConfiguration extends Component return; } } - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $yaml = Yaml::parse($this->value); $yaml = Yaml::dump($yaml, 10, 2); $this->value = $yaml; diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php deleted file mode 100644 index 7439e112f..000000000 --- a/app/Livewire/Settings/Configuration.php +++ /dev/null @@ -1,111 +0,0 @@ - 'nullable', - 'settings.resale_license' => 'nullable', - 'settings.public_port_min' => 'required', - 'settings.public_port_max' => 'required', - 'settings.custom_dns_servers' => 'nullable', - 'settings.instance_name' => 'nullable', - 'settings.allowed_ips' => 'nullable', - ]; - - protected $validationAttributes = [ - 'settings.fqdn' => 'FQDN', - 'settings.resale_license' => 'Resale License', - 'settings.public_port_min' => 'Public port min', - 'settings.public_port_max' => 'Public port max', - 'settings.custom_dns_servers' => 'Custom DNS servers', - 'settings.allowed_ips' => 'Allowed IPs', - ]; - - public function mount() - { - $this->do_not_track = $this->settings->do_not_track; - $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; - $this->is_registration_enabled = $this->settings->is_registration_enabled; - $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; - $this->is_api_enabled = $this->settings->is_api_enabled; - } - - public function instantSave() - { - $this->settings->do_not_track = $this->do_not_track; - $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; - $this->settings->is_registration_enabled = $this->is_registration_enabled; - $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - $this->settings->is_api_enabled = $this->is_api_enabled; - $this->settings->save(); - $this->dispatch('success', 'Settings updated!'); - } - - public function submit() - { - try { - $error_show = false; - $this->server = Server::findOrFail(0); - $this->resetErrorBag(); - if ($this->settings->public_port_min > $this->settings->public_port_max) { - $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); - - return; - } - $this->validate(); - - if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { - if (! validate_dns_entry($this->settings->fqdn, $this->server)) { - $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); - $error_show = true; - } - } - if ($this->settings->fqdn) { - check_domain_usage(domain: $this->settings->fqdn); - } - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); - $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { - return str($dns)->trim()->lower(); - }); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); - $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); - - $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); - $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { - return str($ip)->trim(); - }); - $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); - $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); - - $this->settings->save(); - $this->server->setupDynamicProxyConfiguration(); - if (! $error_show) { - $this->dispatch('success', 'Instance settings updated successfully!'); - } - } catch (\Exception $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index e71d2de00..f593fb78b 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -2,39 +2,170 @@ namespace App\Livewire\Settings; +use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; -use App\Models\S3Storage; -use App\Models\StandalonePostgresql; +use App\Models\Server; use Livewire\Component; class Index extends Component { public InstanceSettings $settings; - public StandalonePostgresql $database; + public bool $do_not_track; - public $s3s; + public bool $is_auto_update_enabled; + + public bool $is_registration_enabled; + + public bool $is_dns_validation_enabled; + + public bool $is_api_enabled; + + public string $auto_update_frequency; + + public string $update_check_frequency; + + protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; + + protected Server $server; + + protected $rules = [ + 'settings.fqdn' => 'nullable', + 'settings.resale_license' => 'nullable', + 'settings.public_port_min' => 'required', + 'settings.public_port_max' => 'required', + 'settings.custom_dns_servers' => 'nullable', + 'settings.instance_name' => 'nullable', + 'settings.allowed_ips' => 'nullable', + 'settings.is_auto_update_enabled' => 'boolean', + 'auto_update_frequency' => 'string', + 'update_check_frequency' => 'string', + ]; + + protected $validationAttributes = [ + 'settings.fqdn' => 'FQDN', + 'settings.resale_license' => 'Resale License', + 'settings.public_port_min' => 'Public port min', + 'settings.public_port_max' => 'Public port max', + 'settings.custom_dns_servers' => 'Custom DNS servers', + 'settings.allowed_ips' => 'Allowed IPs', + 'settings.is_auto_update_enabled' => 'Auto Update Enabled', + 'auto_update_frequency' => 'Auto Update Frequency', + 'update_check_frequency' => 'Update Check Frequency', + ]; public function mount() { if (isInstanceAdmin()) { - $settings = \App\Models\InstanceSettings::get(); - $database = StandalonePostgresql::whereName('coolify-db')->first(); - $s3s = S3Storage::whereTeamId(0)->get() ?? []; - if ($database) { - if ($database->status !== 'running') { - $database->status = 'running'; - $database->save(); - } - $this->database = $database; - } - $this->settings = $settings; - $this->s3s = $s3s; + $this->settings = InstanceSettings::get(); + $this->do_not_track = $this->settings->do_not_track; + $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; + $this->is_registration_enabled = $this->settings->is_registration_enabled; + $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; + $this->is_api_enabled = $this->settings->is_api_enabled; + $this->auto_update_frequency = $this->settings->auto_update_frequency; + $this->update_check_frequency = $this->settings->update_check_frequency; } else { return redirect()->route('dashboard'); } } + public function instantSave() + { + $this->settings->do_not_track = $this->do_not_track; + $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; + $this->settings->is_registration_enabled = $this->is_registration_enabled; + $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; + $this->settings->is_api_enabled = $this->is_api_enabled; + $this->settings->auto_update_frequency = $this->auto_update_frequency; + $this->settings->update_check_frequency = $this->update_check_frequency; + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } + + public function submit() + { + try { + $error_show = false; + $this->server = Server::findOrFail(0); + $this->resetErrorBag(); + if ($this->settings->public_port_min > $this->settings->public_port_max) { + $this->addError('settings.public_port_min', 'The minimum port must be lower than the maximum port.'); + + return; + } + $this->validate(); + + if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.'); + if (empty($this->auto_update_frequency)) { + $this->auto_update_frequency = '0 0 * * *'; + } + + return; + } + + if (! validate_cron_expression($this->update_check_frequency)) { + $this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.'); + if (empty($this->update_check_frequency)) { + $this->update_check_frequency = '0 * * * *'; + } + + return; + } + + if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) { + if (! validate_dns_entry($this->settings->fqdn, $this->server)) { + $this->dispatch('error', "Validating DNS failed.

Make sure you have added the DNS records correctly.

{$this->settings->fqdn}->{$this->server->ip}

Check this documentation for further help."); + $error_show = true; + } + } + if ($this->settings->fqdn) { + check_domain_usage(domain: $this->settings->fqdn); + } + $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim(); + $this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) { + return str($dns)->trim()->lower(); + }); + $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); + $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); + + $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + }); + $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); + $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); + + $this->settings->do_not_track = $this->do_not_track; + $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; + $this->settings->is_registration_enabled = $this->is_registration_enabled; + $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; + $this->settings->is_api_enabled = $this->is_api_enabled; + $this->settings->auto_update_frequency = $this->auto_update_frequency; + $this->settings->update_check_frequency = $this->update_check_frequency; + $this->settings->save(); + $this->server->setupDynamicProxyConfiguration(); + if (! $error_show) { + $this->dispatch('success', 'Instance settings updated successfully!'); + } + } catch (\Exception $e) { + return handleError($e, $this); + } + } + + public function checkManually() + { + CheckForUpdatesJob::dispatchSync(); + $this->dispatch('updateAvailable'); + $settings = InstanceSettings::get(); + if ($settings->new_version_available) { + $this->dispatch('success', 'New version available!'); + } else { + $this->dispatch('success', 'No new version available.'); + } + } + public function render() { return view('livewire.settings.index'); diff --git a/app/Livewire/Settings/Backup.php b/app/Livewire/SettingsBackup.php similarity index 77% rename from app/Livewire/Settings/Backup.php rename to app/Livewire/SettingsBackup.php index 08ad04b2d..da3aee491 100644 --- a/app/Livewire/Settings/Backup.php +++ b/app/Livewire/SettingsBackup.php @@ -1,6 +1,6 @@ backup = $this->database?->scheduledBackups->first() ?? null; - $this->executions = $this->backup?->executions ?? []; + if (isInstanceAdmin()) { + $settings = InstanceSettings::get(); + $database = StandalonePostgresql::whereName('coolify-db')->first(); + $s3s = S3Storage::whereTeamId(0)->get() ?? []; + if ($database) { + if ($database->status !== 'running') { + $database->status = 'running'; + $database->save(); + } + $this->database = $database; + } + $this->settings = $settings; + $this->s3s = $s3s; + $this->backup = $this->database?->scheduledBackups?->first() ?? null; + $this->executions = $this->backup?->executions ?? []; + } else { + return redirect()->route('dashboard'); + } } public function add_coolify_database() diff --git a/app/Livewire/Settings/Email.php b/app/Livewire/SettingsEmail.php similarity index 93% rename from app/Livewire/Settings/Email.php rename to app/Livewire/SettingsEmail.php index bd7f8201e..3eb8ea646 100644 --- a/app/Livewire/Settings/Email.php +++ b/app/Livewire/SettingsEmail.php @@ -1,12 +1,12 @@ emails = auth()->user()->email; + if (isInstanceAdmin()) { + $this->settings = InstanceSettings::get(); + $this->emails = auth()->user()->email; + } else { + return redirect()->route('dashboard'); + } + } public function submitFromFields() diff --git a/app/Livewire/Settings/Auth.php b/app/Livewire/SettingsOauth.php similarity index 95% rename from app/Livewire/Settings/Auth.php rename to app/Livewire/SettingsOauth.php index 783b163e0..c3884589f 100644 --- a/app/Livewire/Settings/Auth.php +++ b/app/Livewire/SettingsOauth.php @@ -1,11 +1,11 @@ 'checkUpdate']; + public function checkUpdate() { - $this->latestVersion = get_latest_version_of_coolify(); - $currentVersion = config('version'); - version_compare($currentVersion, $this->latestVersion, '<') ? $this->isUpgradeAvailable = true : $this->isUpgradeAvailable = false; - if (isDev()) { - $this->isUpgradeAvailable = true; + try { + $settings = InstanceSettings::get(); + $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $this->latestVersion = data_get($versions, 'coolify.v4.version'); + } + $this->isUpgradeAvailable = $settings->new_version_available; + + } catch (\Throwable $e) { + return handleError($e, $this); } + } public function upgrade() diff --git a/app/Models/Application.php b/app/Models/Application.php index 7b39292e0..e2871da4b 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -104,6 +104,8 @@ class Application extends BaseModel protected $guarded = []; + protected $appends = ['server_status']; + protected static function booted() { static::saving(function ($application) { @@ -232,12 +234,24 @@ class Application extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.application.scheduled-tasks', [ + $route = route('project.application.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'application_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; @@ -275,12 +289,20 @@ class Application extends BaseModel return Attribute::make( get: function () { if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (str($this->git_repository)->contains('bitbucket')) { + return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}"; + } + return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; } // Convert the SSH URL to HTTPS URL if (strpos($this->git_repository, 'git@') === 0) { $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (str($this->git_repository)->contains('bitbucket')) { + return "https://{$git_repository}/src/{$this->git_branch}"; + } + return "https://{$git_repository}/tree/{$this->git_branch}"; } @@ -431,6 +453,11 @@ class Application extends BaseModel ); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function isExited() { return (bool) str($this->status)->startsWith('exited'); @@ -441,6 +468,28 @@ class Application extends BaseModel return $this->getRawOriginal('status'); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + if ($this->additional_servers->count() === 0) { + return $this->destination->server->isFunctional(); + } else { + $additional_servers_status = $this->additional_servers->pluck('pivot.status'); + $main_server_status = $this->destination->server->isFunctional(); + foreach ($additional_servers_status as $status) { + $server_status = str($status)->before(':')->value(); + if ($main_server_status !== $server_status) { + return false; + } + } + + return true; + } + } + ); + } + public function status(): Attribute { return Attribute::make( @@ -1270,7 +1319,7 @@ class Application extends BaseModel $template = $this->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 3bdd24014..57d20e3aa 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -35,6 +35,11 @@ class ApplicationPreview extends BaseModel return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail(); } + public function isRunning() + { + return (bool) str($this->status)->startsWith('running'); + } + public function application() { return $this->belongsTo(Application::class); @@ -49,7 +54,7 @@ class ApplicationPreview extends BaseModel $template = $this->application->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn); diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 7e028a6b5..17201ea6e 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -14,7 +14,7 @@ abstract class BaseModel extends Model static::creating(function (Model $model) { // Generate a UUID if one isn't set if (! $model->uuid) { - $model->uuid = (string) new Cuid2(7); + $model->uuid = (string) new Cuid2; } }); } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 28cf0ef93..5e1d8ae13 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -5,7 +5,6 @@ namespace App\Models; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; use OpenApi\Attributes as OA; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; @@ -200,28 +199,33 @@ class EnvironmentVariable extends Model return null; } $environment_variable = trim($environment_variable); - $type = str($environment_variable)->after('{{')->before('.')->value; - if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { - $variable = Str::after($environment_variable, "{$type}."); - $variable = Str::before($variable, '}}'); - $variable = str($variable)->trim()->value; + $sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/'); + if ($sharedEnvsFound->isEmpty()) { + return $environment_variable; + } + foreach ($sharedEnvsFound as $sharedEnv) { + $type = str($sharedEnv)->match('/(.*?)\./'); if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { - return $variable; + continue; } - if ($type === 'environment') { + $variable = str($sharedEnv)->match('/\.(.*)/'); + if ($type->value() === 'environment') { $id = $resource->environment->id; - } elseif ($type === 'project') { + } elseif ($type->value() === 'project') { $id = $resource->environment->project->id; - } else { + } elseif ($type->value() === 'team') { $id = $resource->team()->id; } + if (is_null($id)) { + continue; + } $environment_variable_found = SharedEnvironmentVariable::where('type', $type)->where('key', $variable)->where('team_id', $resource->team()->id)->where("{$type}_id", $id)->first(); if ($environment_variable_found) { - return $environment_variable_found; + $environment_variable = str($environment_variable)->replace("{{{$sharedEnv}}}", $environment_variable_found->value); } } - return $environment_variable; + return str($environment_variable)->value(); } private function get_environment_variables(?string $environment_variable = null): ?string diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index bd3c41a1f..5bd421956 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -18,6 +18,9 @@ class InstanceSettings extends Model implements SendsEmail 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', 'allowed_ip_ranges' => 'array', + 'is_auto_update_enabled' => 'boolean', + 'auto_update_frequency' => 'string', + 'update_check_frequency' => 'string', ]; public function fqdn(): Attribute diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index decfb1a8d..a436f5797 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Events\FileStorageChanged; use Illuminate\Database\Eloquent\Factories\HasFactory; class LocalFileVolume extends BaseModel @@ -33,16 +34,23 @@ class LocalFileVolume extends BaseModel $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; } - $commands = collect([ - "cd $workdir", - ]); + $commands = collect([]); $fs_path = data_get($this, 'fs_path'); + $isFile = instant_remote_process(["test -f $fs_path && echo OK || echo NOK"], $server); + $isDir = instant_remote_process(["test -d $fs_path && echo OK || echo NOK"], $server); if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') { - $commands->push("rm -rf $fs_path"); - } - ray($commands); + ray($isFile, $isDir); + if ($isFile === 'OK') { + $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true"); - return instant_remote_process($commands, $server); + } elseif ($isDir === 'OK') { + $commands->push("rm -rf $fs_path > /dev/null 2>&1 || true"); + $commands->push("rmdir $fs_path > /dev/null 2>&1 || true"); + } + } + if ($commands->count() > 0) { + return instant_remote_process($commands, $server); + } } public function saveStorageOnServer() @@ -55,13 +63,10 @@ class LocalFileVolume extends BaseModel $workdir = $this->resource->workdir(); $server = $this->resource->destination->server; } - $commands = collect([ - "mkdir -p $workdir > /dev/null 2>&1 || true", - "cd $workdir", - ]); - $is_directory = $this->is_directory; - if ($is_directory) { + $commands = collect([]); + if ($this->is_directory) { $commands->push("mkdir -p $this->fs_path > /dev/null 2>&1 || true"); + $commands->push("cd $workdir"); } if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) { $parent_dir = str($this->fs_path)->beforeLast('/'); @@ -79,8 +84,11 @@ class LocalFileVolume extends BaseModel $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) { + $content = instant_remote_process(["cat $path"], $server, false); $fileVolume->is_directory = false; + $fileVolume->content = $content; $fileVolume->save(); + FileStorageChanged::dispatch(data_get($server, 'team_id')); 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; diff --git a/app/Models/Server.php b/app/Models/Server.php index 9166f5a0f..99dca3ecf 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Actions\Server\InstallDocker; use App\Enums\ProxyTypes; use App\Jobs\PullSentinelImageJob; +use App\Notifications\Server\Revived; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Collection; @@ -150,7 +151,7 @@ class Server extends BaseModel $dynamic_conf_path = $this->proxyPath().'/dynamic'; $proxy_type = $this->proxyType(); $redirect_url = $this->proxy->redirect_url; - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml"; } elseif ($proxy_type === 'CADDY') { $default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy"; @@ -180,7 +181,7 @@ respond 404 return; } - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $dynamic_conf = [ 'http' => [ 'routers' => [ @@ -254,7 +255,7 @@ respond 404 { $settings = \App\Models\InstanceSettings::get(); $dynamic_config_path = $this->proxyPath().'/dynamic'; - if ($this->proxyType() === 'TRAEFIK_V2') { + if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { $file = "$dynamic_config_path/coolify.yaml"; if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) { instant_remote_process([ @@ -402,7 +403,7 @@ $schema://$host { // TODO: should use /traefik for already exisiting configurations? // Should move everything except /caddy and /nginx to /traefik // The code needs to be modified as well, so maybe it does not worth it - if ($proxyType === ProxyTypes::TRAEFIK_V2->value) { + if ($proxyType === ProxyTypes::TRAEFIK->value) { $proxy_path = $proxy_path; } elseif ($proxyType === ProxyTypes::CADDY->value) { $proxy_path = $proxy_path.'/caddy'; @@ -420,7 +421,7 @@ $schema://$host { // return $proxyType; // } // if (is_null($proxyType)) { - // $this->proxy->type = ProxyTypes::TRAEFIK_V2->value; + // $this->proxy->type = ProxyTypes::TRAEFIK->value; // $this->proxy->status = ProxyStatus::EXITED->value; // $this->save(); // } @@ -677,7 +678,49 @@ $schema://$host { return instant_remote_process(["docker start $id"], $this); } - public function getContainers(): Collection + public function getContainers() + { + $containers = collect([]); + $containerReplicates = collect([]); + if ($this->isSwarm()) { + $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); + $containers = format_docker_command_output_to_json($containers); + $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false); + if ($containerReplicates) { + $containerReplicates = format_docker_command_output_to_json($containerReplicates); + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $containers = $containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + } else { + $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); + $containers = format_docker_command_output_to_json($containers); + $containerReplicates = collect([]); + } + + return [ + 'containers' => $containers ?? collect([]), + 'containerReplicates' => $containerReplicates ?? collect([]), + ]; + } + + public function getContainersWithSentinel(): Collection { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); @@ -690,21 +733,6 @@ $schema://$host { $containers = data_get(json_decode($containers, true), 'containers', []); return collect($containers); - } else { - if ($this->isSwarm()) { - $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false); - } else { - $containers = instant_remote_process(['docker container ls -q'], $this, false); - if (! $containers) { - return collect([]); - } - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); - } - if (is_null($containers)) { - return collect([]); - } - - return format_docker_command_output_to_json($containers); } } diff --git a/app/Models/Service.php b/app/Models/Service.php index 8336b90c8..33238281e 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -2,11 +2,13 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; use OpenApi\Attributes as OA; +use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; #[OA\Schema( @@ -38,6 +40,8 @@ class Service extends BaseModel protected $guarded = []; + protected $appends = ['server_status']; + public function isConfigurationChanged(bool $save = false) { $domains = $this->applications()->get()->pluck('fqdn')->sort()->toArray(); @@ -76,6 +80,20 @@ class Service extends BaseModel } } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->server->isFunctional(); + } + ); + } + + public function isRunning() + { + return (bool) str($this->status())->contains('running'); + } + public function isExited() { return (bool) str($this->status())->contains('exited'); @@ -575,6 +593,30 @@ class Service extends BaseModel $fields->put('Vaultwarden', $data); break; + case str($image)->contains('gitlab/gitlab'): + $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GITLAB')->first(); + $data = collect([]); + if ($password) { + $data = $data->merge([ + 'Root Password' => [ + 'key' => data_get($password, 'key'), + 'value' => data_get($password, 'value'), + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + } + $data = $data->merge([ + 'Root User' => [ + 'key' => 'N/A', + 'value' => 'root', + 'rules' => 'required', + 'isPassword' => true, + ], + ]); + + $fields->put('GitLab', $data->toArray()); + break; } } $databases = $this->databases()->get(); @@ -764,12 +806,24 @@ class Service extends BaseModel public function failedTaskLink($task_uuid) { if (data_get($this, 'environment.project.uuid')) { - return route('project.service.scheduled-tasks', [ + $route = route('project.service.scheduled-tasks', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_name' => data_get($this, 'environment.name'), 'service_uuid' => data_get($this, 'uuid'), 'task_uuid' => $task_uuid, ]); + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $url = Url::fromString($route); + $url = $url->withPort(null); + $fqdn = data_get($settings, 'fqdn'); + $fqdn = str_replace(['http://', 'https://'], '', $fqdn); + $url = $url->withHost($fqdn); + + return $url->__toString(); + } + + return $route; } return null; diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index f930226da..4cd194cd8 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -14,7 +14,7 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ 'clickhouse_password' => 'encrypted', @@ -40,6 +40,15 @@ class StandaloneClickhouse extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings; diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index be04e2d83..8726b2546 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -14,7 +14,7 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ 'dragonfly_password' => 'encrypted', @@ -40,6 +40,15 @@ class StandaloneDragonfly extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings; diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 1b7d5a958..607cacade 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -14,7 +14,7 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url']; + protected $appends = ['internal_db_url', 'external_db_url', 'server_status']; protected $casts = [ 'keydb_password' => 'encrypted', @@ -40,6 +40,15 @@ class StandaloneKeydb extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->keydb_conf; diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 2081d9c89..d88653e41 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -14,7 +14,7 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ 'mariadb_password' => 'encrypted', @@ -40,6 +40,15 @@ class StandaloneMariadb extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->mariadb_conf; diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 066b34ab7..f09e932bf 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -14,7 +14,7 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected static function booted() { @@ -44,6 +44,15 @@ class StandaloneMongodb extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->mongo_conf; diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 375b56133..f4e56fab2 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -14,7 +14,7 @@ class StandaloneMysql extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ 'mysql_password' => 'encrypted', @@ -41,6 +41,15 @@ class StandaloneMysql extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->mysql_conf; diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index ab6bcc626..311c09c36 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -14,7 +14,7 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ 'init_scripts' => 'array', @@ -46,6 +46,15 @@ class StandalonePostgresql extends BaseModel return database_configuration_dir()."/{$this->uuid}"; } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function delete_configurations() { $server = data_get($this, 'destination.server'); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index df6cc8aeb..8a202ea9e 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -14,7 +14,7 @@ class StandaloneRedis extends BaseModel protected $guarded = []; - protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected static function booted() { @@ -36,6 +36,15 @@ class StandaloneRedis extends BaseModel }); } + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->redis_conf; diff --git a/app/Models/User.php b/app/Models/User.php index 3625b9930..ecc4ef6b6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -180,6 +180,10 @@ class User extends Authenticatable implements SendsEmail { $found_root_team = auth()->user()->teams->filter(function ($team) { if ($team->id == 0) { + if (! auth()->user()->isAdmin()) { + return false; + } + return true; } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php index e05e13e9b..3f2b3b696 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Revived.php @@ -12,6 +12,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\RateLimiter; class Revived extends Notification implements ShouldQueue { @@ -44,8 +45,20 @@ class Revived extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + $executed = RateLimiter::attempt( + 'notification-server-revived-'.$this->server->uuid, + 1, + function () use ($channels) { + return $channels; + }, + 7200, + ); - return $channels; + if (! $executed) { + return []; + } + + return $executed; } public function toMail(): MailMessage diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index f178c9be3..2fb83559a 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -10,6 +10,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Support\Facades\RateLimiter; class Unreachable extends Notification implements ShouldQueue { @@ -35,8 +36,20 @@ class Unreachable extends Notification implements ShouldQueue if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } + $executed = RateLimiter::attempt( + 'notification-server-unreachable-'.$this->server->uuid, + 1, + function () use ($channels) { + return $channels; + }, + 7200, + ); - return $channels; + if (! $executed) { + return []; + } + + return $executed; } public function toMail(): MailMessage diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 9b0a81026..53a2e9281 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -44,6 +44,8 @@ class FortifyServiceProvider extends ServiceProvider { Fortify::createUsersUsing(CreateNewUser::class); Fortify::registerView(function () { + $isFirstUser = User::count() === 0; + $settings = \App\Models\InstanceSettings::get(); if (! $settings->is_registration_enabled) { return redirect()->route('login'); @@ -51,7 +53,9 @@ class FortifyServiceProvider extends ServiceProvider if (config('coolify.waitlist')) { return redirect()->route('waitlist.index'); } else { - return view('auth.register'); + return view('auth.register', [ + 'isFirstUser' => $isFirstUser, + ]); } }); diff --git a/app/View/Components/Forms/Datalist.php b/app/View/Components/Forms/Datalist.php index df0c1cb11..25643753d 100644 --- a/app/View/Components/Forms/Datalist.php +++ b/app/View/Components/Forms/Datalist.php @@ -30,7 +30,7 @@ class Datalist extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 35448d5e5..6c9378cac 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -27,7 +27,7 @@ class Input extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Select.php b/app/View/Components/Forms/Select.php index 21c147c2b..dd5ba66b7 100644 --- a/app/View/Components/Forms/Select.php +++ b/app/View/Components/Forms/Select.php @@ -30,7 +30,7 @@ class Select extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 7d1860500..3f887877c 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -41,7 +41,7 @@ class Textarea extends Component public function render(): View|Closure|string { if (is_null($this->id)) { - $this->id = new Cuid2(7); + $this->id = new Cuid2; } if (is_null($this->name)) { $this->name = $this->id; diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 5bfdcce78..b8dcc1f3c 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -14,7 +14,7 @@ use Visus\Cuid2\Cuid2; function generate_database_name(string $type): string { - $cuid = new Cuid2(7); + $cuid = new Cuid2; return $type.'-database-'.$cuid; } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 0aa5c6b74..a534dc5ff 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -1,5 +1,6 @@ $domain) { try { if ($generate_unique_uuid) { - $uuid = new Cuid2(7); + $uuid = new Cuid2; } $url = Url::fromString($domain); @@ -539,26 +540,55 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview if ($pull_request_id === 0) { if ($application->fqdn) { $domains = str(data_get($application, 'fqdn'))->explode(','); - $labels = $labels->merge(fqdnLabelsForTraefik( - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect - )); - // Add Caddy labels - $labels = $labels->merge(fqdnLabelsForCaddy( - network: $application->destination->network, - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled(), - redirect_direction: $application->redirect - )); + $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($application->destination->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + break; + case ProxyTypes::CADDY->value: + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + break; + } + } else { + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled(), + redirect_direction: $application->redirect + )); + } + } } else { if (data_get($preview, 'fqdn')) { @@ -566,24 +596,50 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview } else { $domains = collect([]); } - $labels = $labels->merge(fqdnLabelsForTraefik( - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() - )); - // Add Caddy labels - $labels = $labels->merge(fqdnLabelsForCaddy( - network: $application->destination->network, - uuid: $appUuid, - domains: $domains, - onlyPort: $onlyPort, - is_force_https_enabled: $application->isForceHttpsEnabled(), - is_gzip_enabled: $application->isGzipEnabled(), - is_stripprefix_enabled: $application->isStripprefixEnabled() - )); + $shouldGenerateLabelsExactly = $application->destination->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($application->destination->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + break; + case ProxyTypes::CADDY->value: + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + break; + } + } else { + $labels = $labels->merge(fqdnLabelsForTraefik( + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + $labels = $labels->merge(fqdnLabelsForCaddy( + network: $application->destination->network, + uuid: $appUuid, + domains: $domains, + onlyPort: $onlyPort, + is_force_https_enabled: $application->isForceHttpsEnabled(), + is_gzip_enabled: $application->isGzipEnabled(), + is_stripprefix_enabled: $application->isStripprefixEnabled() + )); + } } diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 23c9a2333..f18b09f56 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -24,6 +24,7 @@ function collectProxyDockerNetworksByServer(Server $server) } function collectDockerNetworksByServer(Server $server) { + $allNetworks = collect([]); if ($server->isSwarm()) { $networks = collect($server->swarmDockers)->map(function ($docker) { return $docker['network']; @@ -34,18 +35,28 @@ function collectDockerNetworksByServer(Server $server) return $docker['network']; }); } + $allNetworks = $allNetworks->merge($networks); // Service networks foreach ($server->services()->get() as $service) { - $networks->push($service->networks()); + if ($service->isRunning()) { + $networks->push($service->networks()); + } + $allNetworks->push($service->networks()); } // Docker compose based apps $docker_compose_apps = $server->dockerComposeBasedApplications(); foreach ($docker_compose_apps as $app) { - $networks->push($app->uuid); + if ($app->isRunning()) { + $networks->push($app->uuid); + } + $allNetworks->push($app->uuid); } // Docker compose based preview deployments $docker_compose_previews = $server->dockerComposeBasedPreviewDeployments(); foreach ($docker_compose_previews as $preview) { + if (! $preview->isRunning()) { + continue; + } $pullRequestId = $preview->pull_request_id; $applicationId = $preview->application_id; $application = Application::find($applicationId); @@ -54,23 +65,30 @@ function collectDockerNetworksByServer(Server $server) } $network = "{$application->uuid}-{$pullRequestId}"; $networks->push($network); + $allNetworks->push($network); } $networks = collect($networks)->flatten()->unique(); + $allNetworks = $allNetworks->flatten()->unique(); if ($server->isSwarm()) { if ($networks->count() === 0) { $networks = collect(['coolify-overlay']); + $allNetworks = collect(['coolify-overlay']); } } else { if ($networks->count() === 0) { $networks = collect(['coolify']); + $allNetworks = collect(['coolify']); } } - return $networks; + return [ + 'networks' => $networks, + 'allNetworks' => $allNetworks, + ]; } function connectProxyToNetworks(Server $server) { - $networks = collectDockerNetworksByServer($server); + ['networks' => $networks] = collectDockerNetworksByServer($server); if ($server->isSwarm()) { $commands = $networks->map(function ($network) { return [ @@ -118,7 +136,7 @@ function generate_default_proxy_configuration(Server $server) 'external' => true, ]; }); - if ($proxy_type === 'TRAEFIK_V2') { + if ($proxy_type === ProxyTypes::TRAEFIK->value) { $labels = [ 'traefik.enable=true', 'traefik.http.routers.traefik.entrypoints=http', diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index d4e9ebcde..1cc2ac36d 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -78,7 +78,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolume->is_directory = true; $fileVolume->save(); instant_remote_process(["mkdir -p $fileLocation"], $server); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && ! $content) { + } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($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; @@ -122,14 +122,22 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $path = $fqdn->getPath(); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost(); if ($generatedEnv) { - $generatedEnv->value = $fqdn.$path; + if ($path === '/') { + $generatedEnv->value = $fqdn; + } else { + $generatedEnv->value = $fqdn.$path; + } $generatedEnv->save(); } if ($port) { $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); if ($generatedEnv) { - $generatedEnv->value = $fqdn.$path; + if ($path === '/') { + $generatedEnv->value = $fqdn; + } else { + $generatedEnv->value = $fqdn.$path; + } $generatedEnv->save(); } } @@ -141,14 +149,22 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $url = $url->getHost(); if ($generatedEnv) { $url = str($fqdn)->after('://'); - $generatedEnv->value = $url.$path; + if ($path === '/') { + $generatedEnv->value = $url; + } else { + $generatedEnv->value = $url.$path; + } $generatedEnv->save(); } if ($port) { $variableName = $variableName."_$port"; $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); if ($generatedEnv) { - $generatedEnv->value = $url.$path; + if ($path === '/') { + $generatedEnv->value = $url; + } else { + $generatedEnv->value = $url.$path; + } $generatedEnv->save(); } } @@ -165,10 +181,18 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $service_fqdn = str($port_env->key)->beforeLast('_')->after('SERVICE_FQDN_'); $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_FQDN_'.$service_fqdn)->first(); if ($env) { - $env->value = $host.$path; + if ($path === '/') { + $env->value = $host; + } else { + $env->value = $host.$path; + } $env->save(); } - $port_env->value = $host.$path; + if ($path === '/') { + $port_env->value = $host; + } else { + $port_env->value = $host.$path; + } $port_env->save(); } $port_envs_url = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'like', "SERVICE_URL_%_$port")->get(); @@ -176,10 +200,18 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $service_url = str($port_env_url->key)->beforeLast('_')->after('SERVICE_URL_'); $env = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', 'SERVICE_URL_'.$service_url)->first(); if ($env) { - $env->value = $url.$path; + if ($path === '/') { + $env->value = $url; + } else { + $env->value = $url.$path; + } $env->save(); } - $port_env_url->value = $url.$path; + if ($path === '/') { + $port_env_url->value = $url; + } else { + $port_env_url->value = $url.$path; + } $port_env_url->save(); } } else { diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 56487c8a6..aae4fafd4 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1,6 +1,7 @@ json(); - // return data_get($versions, 'coolify.v4.version'); } catch (\Throwable $e) { - //throw $e; ray($e->getMessage()); return '0.0.0'; @@ -200,7 +197,7 @@ function generate_random_name(?string $cuid = null): string ] ); if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("{$generator->getName()}-$cuid"); @@ -236,7 +233,7 @@ function formatPrivateKey(string $privateKey) function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string { if (is_null($cuid)) { - $cuid = new Cuid2(7); + $cuid = new Cuid2; } return Str::kebab("$git_repository:$git_branch-$cuid"); @@ -505,7 +502,7 @@ function get_service_templates(bool $force = false): Collection { if ($force) { try { - $response = Http::retry(3, 50)->get(config('constants.services.official')); + $response = Http::retry(3, 1000)->get(config('constants.services.official')); if ($response->failed()) { return collect([]); } @@ -1308,27 +1305,58 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $serviceLabels = $serviceLabels->merge($defaultLabels); if (! $isDatabase && $fqdns->count() > 0) { if ($fqdns) { - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - is_force_https_enabled: true, - serviceLabels: $serviceLabels, - is_gzip_enabled: $savedService->isGzipEnabled(), - is_stripprefix_enabled: $savedService->isStripprefixEnabled(), - service_name: $serviceName, - image: data_get($service, 'image') - )); + $shouldGenerateLabelsExactly = $resource->server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($resource->server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + break; + } + } else { + $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + is_force_https_enabled: true, + serviceLabels: $serviceLabels, + is_gzip_enabled: $savedService->isGzipEnabled(), + is_stripprefix_enabled: $savedService->isStripprefixEnabled(), + service_name: $serviceName, + image: data_get($service, 'image') + )); + } } } if ($resource->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) { @@ -2022,7 +2050,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $template = $resource->preview_url_template; $host = $url->getHost(); $schema = $url->getScheme(); - $random = new Cuid2(7); + $random = new Cuid2; $preview_fqdn = str_replace('{{random}}', $random, $template); $preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn); $preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn); @@ -2034,26 +2062,64 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal }); } } - $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik( - uuid: $resource->uuid, - domains: $fqdns, - serviceLabels: $serviceLabels, - generate_unique_uuid: $resource->build_pack === 'dockercompose', - image: data_get($service, 'image'), - is_force_https_enabled: $resource->isForceHttpsEnabled(), - is_gzip_enabled: $resource->isGzipEnabled(), - is_stripprefix_enabled: $resource->isStripprefixEnabled(), - )); - $serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy( - network: $resource->destination->network, - uuid: $resource->uuid, - domains: $fqdns, - serviceLabels: $serviceLabels, - image: data_get($service, 'image'), - is_force_https_enabled: $resource->isForceHttpsEnabled(), - is_gzip_enabled: $resource->isGzipEnabled(), - is_stripprefix_enabled: $resource->isStripprefixEnabled(), - )); + $shouldGenerateLabelsExactly = $server->settings->generate_exact_labels; + if ($shouldGenerateLabelsExactly) { + switch ($server->proxyType()) { + case ProxyTypes::TRAEFIK->value: + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + generate_unique_uuid: $resource->build_pack === 'dockercompose', + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + break; + case ProxyTypes::CADDY->value: + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + break; + } + } else { + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForTraefik( + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + generate_unique_uuid: $resource->build_pack === 'dockercompose', + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + $serviceLabels = $serviceLabels->merge( + fqdnLabelsForCaddy( + network: $resource->destination->network, + uuid: $resource->uuid, + domains: $fqdns, + serviceLabels: $serviceLabels, + image: data_get($service, 'image'), + is_force_https_enabled: $resource->isForceHttpsEnabled(), + is_gzip_enabled: $resource->isGzipEnabled(), + is_stripprefix_enabled: $resource->isStripprefixEnabled(), + ) + ); + } } } } diff --git a/config/sentry.php b/config/sentry.php index b2f6ded80..a27a18d30 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // 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.319', + 'release' => '4.0.0-beta.320', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 8b5a5afc6..05acb11ca 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('auto_update_frequency')->default('0 0 * * *'); + $table->string('update_check_frequency')->default('0 * * * *'); + $table->boolean('new_version_available')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('update_check_frequency'); + $table->dropColumn('auto_update_frequency'); + $table->dropColumn('new_version_available'); + }); + } +}; diff --git a/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php b/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php new file mode 100644 index 000000000..7bc8a0657 --- /dev/null +++ b/database/migrations/2024_08_07_155324_add_proxy_label_chooser.php @@ -0,0 +1,28 @@ +boolean('generate_exact_labels')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('generate_exact_labels'); + }); + } +}; diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index e030c0ae6..c88a35f6a 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -100,7 +100,7 @@ class ProductionSeeder extends Seeder 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); @@ -153,7 +153,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== 'private_key_id' => 0, ]; $server_details['proxy'] = ServerMetadata::from([ - 'type' => ProxyTypes::TRAEFIK_V2->value, + 'type' => ProxyTypes::TRAEFIK->value, 'status' => ProxyStatus::EXITED->value, ]); $server = Server::create($server_details); diff --git a/openapi.yaml b/openapi.yaml index a63f830b7..00d7bff43 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5,6 +5,7 @@ info: servers: - url: 'https://app.coolify.io/api/v1' + description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.' paths: /applications: get: @@ -29,225 +30,6 @@ paths: security: - bearerAuth: [] - patch: - tags: - - Applications - summary: Update - description: 'Update application by UUID.' - operationId: ff28a22d25b1f658c40b54d2073abbca - requestBody: - description: 'Application updated.' - required: true - content: - application/json: - schema: - properties: - project_uuid: - type: string - description: 'The project UUID.' - server_uuid: - type: string - description: 'The server UUID.' - environment_name: - type: string - description: 'The environment name.' - github_app_uuid: - type: string - description: 'The Github App UUID.' - git_repository: - type: string - description: 'The git repository URL.' - git_branch: - type: string - description: 'The git branch.' - ports_exposes: - type: string - description: 'The ports to expose.' - destination_uuid: - type: string - description: 'The destination UUID.' - build_pack: - type: string - enum: [nixpacks, static, dockerfile, dockercompose] - description: 'The build pack type.' - name: - type: string - description: 'The application name.' - description: - type: string - description: 'The application description.' - domains: - type: string - description: 'The application domains.' - git_commit_sha: - type: string - description: 'The git commit SHA.' - docker_registry_image_name: - type: string - description: 'The docker registry image name.' - docker_registry_image_tag: - type: string - description: 'The docker registry image tag.' - is_static: - type: boolean - description: 'The flag to indicate if the application is static.' - install_command: - type: string - description: 'The install command.' - build_command: - type: string - description: 'The build command.' - start_command: - type: string - description: 'The start command.' - ports_mappings: - type: string - description: 'The ports mappings.' - base_directory: - type: string - description: 'The base directory for all commands.' - publish_directory: - type: string - description: 'The publish directory.' - health_check_enabled: - type: boolean - description: 'Health check enabled.' - health_check_path: - type: string - description: 'Health check path.' - health_check_port: - type: string - nullable: true - description: 'Health check port.' - health_check_host: - type: string - nullable: true - description: 'Health check host.' - health_check_method: - type: string - description: 'Health check method.' - health_check_return_code: - type: integer - description: 'Health check return code.' - health_check_scheme: - type: string - description: 'Health check scheme.' - health_check_response_text: - type: string - nullable: true - description: 'Health check response text.' - health_check_interval: - type: integer - description: 'Health check interval in seconds.' - health_check_timeout: - type: integer - description: 'Health check timeout in seconds.' - health_check_retries: - type: integer - description: 'Health check retries count.' - health_check_start_period: - type: integer - description: 'Health check start period in seconds.' - limits_memory: - type: string - description: 'Memory limit.' - limits_memory_swap: - type: string - description: 'Memory swap limit.' - limits_memory_swappiness: - type: integer - description: 'Memory swappiness.' - limits_memory_reservation: - type: string - description: 'Memory reservation.' - limits_cpus: - type: string - description: 'CPU limit.' - limits_cpuset: - type: string - nullable: true - description: 'CPU set.' - limits_cpu_shares: - type: integer - description: 'CPU shares.' - custom_labels: - type: string - description: 'Custom labels.' - custom_docker_run_options: - type: string - description: 'Custom docker run options.' - post_deployment_command: - type: string - description: 'Post deployment command.' - post_deployment_command_container: - type: string - description: 'Post deployment command container.' - pre_deployment_command: - type: string - description: 'Pre deployment command.' - pre_deployment_command_container: - type: string - description: 'Pre deployment command container.' - manual_webhook_secret_github: - type: string - description: 'Manual webhook secret for Github.' - manual_webhook_secret_gitlab: - type: string - description: 'Manual webhook secret for Gitlab.' - manual_webhook_secret_bitbucket: - type: string - description: 'Manual webhook secret for Bitbucket.' - manual_webhook_secret_gitea: - type: string - description: 'Manual webhook secret for Gitea.' - redirect: - type: string - nullable: true - description: 'How to set redirect with Traefik / Caddy. www<->non-www.' - enum: [www, non-www, both] - instant_deploy: - type: boolean - description: 'The flag to indicate if the application should be deployed instantly.' - dockerfile: - type: string - description: 'The Dockerfile content.' - docker_compose_location: - type: string - description: 'The Docker Compose location.' - docker_compose_raw: - type: string - description: 'The Docker Compose raw content.' - docker_compose_custom_start_command: - type: string - description: 'The Docker Compose custom start command.' - docker_compose_custom_build_command: - type: string - description: 'The Docker Compose custom build command.' - docker_compose_domains: - type: array - description: 'The Docker Compose domains.' - watch_paths: - type: string - description: 'The watch paths.' - type: object - responses: - '200': - description: 'Application updated.' - content: - application/json: - schema: - properties: - uuid: { type: string } - type: object - '401': - $ref: '#/components/responses/401' - '400': - $ref: '#/components/responses/400' - '404': - $ref: '#/components/responses/404' - security: - - - bearerAuth: [] /applications/public: post: tags: @@ -1369,6 +1151,225 @@ paths: security: - bearerAuth: [] + patch: + tags: + - Applications + summary: Update + description: 'Update application by UUID.' + operationId: 62a3b1775e8cba5d39a236ebb69830b7 + requestBody: + description: 'Application updated.' + required: true + content: + application/json: + schema: + properties: + project_uuid: + type: string + description: 'The project UUID.' + server_uuid: + type: string + description: 'The server UUID.' + environment_name: + type: string + description: 'The environment name.' + github_app_uuid: + type: string + description: 'The Github App UUID.' + git_repository: + type: string + description: 'The git repository URL.' + git_branch: + type: string + description: 'The git branch.' + ports_exposes: + type: string + description: 'The ports to expose.' + destination_uuid: + type: string + description: 'The destination UUID.' + build_pack: + type: string + enum: [nixpacks, static, dockerfile, dockercompose] + description: 'The build pack type.' + name: + type: string + description: 'The application name.' + description: + type: string + description: 'The application description.' + domains: + type: string + description: 'The application domains.' + git_commit_sha: + type: string + description: 'The git commit SHA.' + docker_registry_image_name: + type: string + description: 'The docker registry image name.' + docker_registry_image_tag: + type: string + description: 'The docker registry image tag.' + is_static: + type: boolean + description: 'The flag to indicate if the application is static.' + install_command: + type: string + description: 'The install command.' + build_command: + type: string + description: 'The build command.' + start_command: + type: string + description: 'The start command.' + ports_mappings: + type: string + description: 'The ports mappings.' + base_directory: + type: string + description: 'The base directory for all commands.' + publish_directory: + type: string + description: 'The publish directory.' + health_check_enabled: + type: boolean + description: 'Health check enabled.' + health_check_path: + type: string + description: 'Health check path.' + health_check_port: + type: string + nullable: true + description: 'Health check port.' + health_check_host: + type: string + nullable: true + description: 'Health check host.' + health_check_method: + type: string + description: 'Health check method.' + health_check_return_code: + type: integer + description: 'Health check return code.' + health_check_scheme: + type: string + description: 'Health check scheme.' + health_check_response_text: + type: string + nullable: true + description: 'Health check response text.' + health_check_interval: + type: integer + description: 'Health check interval in seconds.' + health_check_timeout: + type: integer + description: 'Health check timeout in seconds.' + health_check_retries: + type: integer + description: 'Health check retries count.' + health_check_start_period: + type: integer + description: 'Health check start period in seconds.' + limits_memory: + type: string + description: 'Memory limit.' + limits_memory_swap: + type: string + description: 'Memory swap limit.' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness.' + limits_memory_reservation: + type: string + description: 'Memory reservation.' + limits_cpus: + type: string + description: 'CPU limit.' + limits_cpuset: + type: string + nullable: true + description: 'CPU set.' + limits_cpu_shares: + type: integer + description: 'CPU shares.' + custom_labels: + type: string + description: 'Custom labels.' + custom_docker_run_options: + type: string + description: 'Custom docker run options.' + post_deployment_command: + type: string + description: 'Post deployment command.' + post_deployment_command_container: + type: string + description: 'Post deployment command container.' + pre_deployment_command: + type: string + description: 'Pre deployment command.' + pre_deployment_command_container: + type: string + description: 'Pre deployment command container.' + manual_webhook_secret_github: + type: string + description: 'Manual webhook secret for Github.' + manual_webhook_secret_gitlab: + type: string + description: 'Manual webhook secret for Gitlab.' + manual_webhook_secret_bitbucket: + type: string + description: 'Manual webhook secret for Bitbucket.' + manual_webhook_secret_gitea: + type: string + description: 'Manual webhook secret for Gitea.' + redirect: + type: string + nullable: true + description: 'How to set redirect with Traefik / Caddy. www<->non-www.' + enum: [www, non-www, both] + instant_deploy: + type: boolean + description: 'The flag to indicate if the application should be deployed instantly.' + dockerfile: + type: string + description: 'The Dockerfile content.' + docker_compose_location: + type: string + description: 'The Docker Compose location.' + docker_compose_raw: + type: string + description: 'The Docker Compose raw content.' + docker_compose_custom_start_command: + type: string + description: 'The Docker Compose custom start command.' + docker_compose_custom_build_command: + type: string + description: 'The Docker Compose custom build command.' + docker_compose_domains: + type: array + description: 'The Docker Compose domains.' + watch_paths: + type: string + description: 'The watch paths.' + type: object + responses: + '200': + description: 'Application updated.' + content: + application/json: + schema: + properties: + uuid: { type: string } + type: object + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '404': + $ref: '#/components/responses/404' + security: + - + bearerAuth: [] '/applications/{uuid}/envs': get: tags: @@ -2833,7 +2834,7 @@ paths: description: 'Deployment Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Get deployment by UUID.' @@ -3062,7 +3063,7 @@ paths: description: 'Project UUID' required: true schema: - type: integer + type: string responses: '200': description: 'Project details' @@ -3166,7 +3167,7 @@ paths: description: 'Project UUID' required: true schema: - type: integer + type: string - name: environment_name in: path @@ -3325,7 +3326,7 @@ paths: description: 'Private Key Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Get all private keys.' @@ -3357,7 +3358,7 @@ paths: description: 'Private Key Uuid' required: true schema: - type: integer + type: string responses: '200': description: 'Private Key deleted.' @@ -3477,7 +3478,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get server by UUID' @@ -3597,7 +3598,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get resources by server' @@ -3629,7 +3630,7 @@ paths: description: "Server's Uuid" required: true schema: - type: integer + type: string responses: '200': description: 'Get domains by server' @@ -3661,7 +3662,7 @@ paths: description: 'Server UUID' required: true schema: - type: integer + type: string responses: '201': description: 'Server validation started.' diff --git a/public/svgs/gitlab.svg b/public/svgs/gitlab.svg new file mode 100644 index 000000000..1c7cb0719 --- /dev/null +++ b/public/svgs/gitlab.svg @@ -0,0 +1 @@ + diff --git a/public/svgs/minecraft.svg b/public/svgs/minecraft.svg new file mode 100644 index 000000000..10ae9042c --- /dev/null +++ b/public/svgs/minecraft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 297640111..1d236a6c4 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -16,9 +16,16 @@ $email = getOldOrLocal('email', 'test3@example.com');
-

- Create an account -

+
+

+ Create an account +

+ @if ($isFirstUser) +
This user will be the root user (full admin access). +
+ @endif +
@csrf @endif + @if (!isSubscribed() && isCloud() && auth()->user()->teams()->get()->count() > 1) + + @endif
  • @@ -380,44 +383,5 @@
  • - {{--
  • -
    Your teams
    - -
  • -
  • - - - Your profile - - -
  • --}} diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 2b06ba7e6..8a6400aef 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -5,11 +5,11 @@ @if ($server->proxyType() !== 'NONE') - {{-- @if ($server->proxyType() === 'TRAEFIK_V2') --}} - - - + {{-- @if ($server->proxyType() === 'TRAEFIK') --}} + + + {{-- @endif --}} diff --git a/resources/views/components/settings/navbar.blade.php b/resources/views/components/settings/navbar.blade.php index 7b04b66b9..4839893e0 100644 --- a/resources/views/components/settings/navbar.blade.php +++ b/resources/views/components/settings/navbar.blade.php @@ -13,6 +13,18 @@ @endif + + + + + + + + +
    diff --git a/resources/views/livewire/navbar-delete-team.blade.php b/resources/views/livewire/navbar-delete-team.blade.php new file mode 100644 index 000000000..b660a0dd4 --- /dev/null +++ b/resources/views/livewire/navbar-delete-team.blade.php @@ -0,0 +1,5 @@ +
    + + This team be deleted. It is not reversible.
    Please think again. +
    +
    diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index ada74260f..b5ba6c822 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -41,6 +41,14 @@ @endif + @if ($application->server_status == false) + + + + + + @endif +