Merge branch 'next' into fix/improved-responses-on-git-error
This commit is contained in:
@@ -6,50 +6,159 @@ use App\Enums\ApplicationDeploymentStatus;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
use RuntimeException;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Application model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The application identifier in the database.'],
|
||||
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'The application description.'],
|
||||
'repository_project_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'The repository project identifier.'],
|
||||
'uuid' => ['type' => 'string', 'description' => 'The application UUID.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'fqdn' => ['type' => 'string', 'nullable' => true, 'description' => 'The application domains.'],
|
||||
'config_hash' => ['type' => 'string', 'description' => 'Configuration hash.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'Git repository URL.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'Git branch.'],
|
||||
'git_commit_sha' => ['type' => 'string', 'description' => 'Git commit SHA.'],
|
||||
'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'],
|
||||
'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'],
|
||||
'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'],
|
||||
'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']],
|
||||
'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'],
|
||||
'install_command' => ['type' => 'string', 'description' => 'Install command.'],
|
||||
'build_command' => ['type' => 'string', 'description' => 'Build command.'],
|
||||
'start_command' => ['type' => 'string', 'description' => 'Start command.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'],
|
||||
'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'],
|
||||
'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'],
|
||||
'publish_directory' => ['type' => 'string', 'description' => '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.'],
|
||||
'status' => ['type' => 'string', 'description' => 'Application status.'],
|
||||
'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'],
|
||||
'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
|
||||
'destination_id' => ['type' => 'integer', 'description' => 'Destination identifier.'],
|
||||
'source_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Source identifier.'],
|
||||
'private_key_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Private key identifier.'],
|
||||
'environment_id' => ['type' => 'integer', 'description' => 'Environment identifier.'],
|
||||
'dockerfile' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile content. Used for dockerfile build pack.'],
|
||||
'dockerfile_location' => ['type' => 'string', 'description' => 'Dockerfile location.'],
|
||||
'custom_labels' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom labels.'],
|
||||
'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile target build.'],
|
||||
'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitHub.'],
|
||||
'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitLab.'],
|
||||
'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Bitbucket.'],
|
||||
'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Gitea.'],
|
||||
'docker_compose_location' => ['type' => 'string', 'description' => 'Docker compose location.'],
|
||||
'docker_compose' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose content. Used for docker compose build pack.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose raw content.'],
|
||||
'docker_compose_domains' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose domains.'],
|
||||
'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom start command.'],
|
||||
'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom build command.'],
|
||||
'swarm_replicas' => ['type' => 'integer', 'nullable' => true, 'description' => 'Swarm replicas. Only used for swarm deployments.'],
|
||||
'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true, 'description' => 'Swarm placement constraints. Only used for swarm deployments.'],
|
||||
'custom_docker_run_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom docker run options.'],
|
||||
'post_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command.'],
|
||||
'post_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command container.'],
|
||||
'pre_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command.'],
|
||||
'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command container.'],
|
||||
'watch_paths' => ['type' => 'string', 'nullable' => true, 'description' => 'Watch paths.'],
|
||||
'custom_healthcheck_found' => ['type' => 'boolean', 'description' => 'Custom healthcheck found.'],
|
||||
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
|
||||
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was created.'],
|
||||
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'],
|
||||
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
|
||||
'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
|
||||
'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'],
|
||||
]
|
||||
)]
|
||||
|
||||
class Application extends BaseModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
private static $parserVersion = '4';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['server_status'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($application) {
|
||||
if ($application->fqdn == '') {
|
||||
$application->fqdn = null;
|
||||
$payload = [];
|
||||
if ($application->isDirty('fqdn')) {
|
||||
if ($application->fqdn === '') {
|
||||
$application->fqdn = null;
|
||||
}
|
||||
$payload['fqdn'] = $application->fqdn;
|
||||
}
|
||||
if ($application->isDirty('install_command')) {
|
||||
$payload['install_command'] = str($application->install_command)->trim();
|
||||
}
|
||||
if ($application->isDirty('build_command')) {
|
||||
$payload['build_command'] = str($application->build_command)->trim();
|
||||
}
|
||||
if ($application->isDirty('start_command')) {
|
||||
$payload['start_command'] = str($application->start_command)->trim();
|
||||
}
|
||||
if ($application->isDirty('base_directory')) {
|
||||
$payload['base_directory'] = str($application->base_directory)->trim();
|
||||
}
|
||||
if ($application->isDirty('publish_directory')) {
|
||||
$payload['publish_directory'] = str($application->publish_directory)->trim();
|
||||
}
|
||||
if ($application->isDirty('status')) {
|
||||
$payload['last_online_at'] = now();
|
||||
}
|
||||
if ($application->isDirty('custom_nginx_configuration')) {
|
||||
if ($application->custom_nginx_configuration === '') {
|
||||
$payload['custom_nginx_configuration'] = null;
|
||||
}
|
||||
}
|
||||
if (count($payload) > 0) {
|
||||
$application->forceFill($payload);
|
||||
}
|
||||
$application->forceFill([
|
||||
'fqdn' => $application->fqdn,
|
||||
'install_command' => Str::of($application->install_command)->trim(),
|
||||
'build_command' => Str::of($application->build_command)->trim(),
|
||||
'start_command' => Str::of($application->start_command)->trim(),
|
||||
'base_directory' => Str::of($application->base_directory)->trim(),
|
||||
'publish_directory' => Str::of($application->publish_directory)->trim(),
|
||||
]);
|
||||
});
|
||||
static::created(function ($application) {
|
||||
ApplicationSetting::create([
|
||||
'application_id' => $application->id,
|
||||
]);
|
||||
$application->compose_parsing_version = self::$parserVersion;
|
||||
$application->save();
|
||||
});
|
||||
static::deleting(function ($application) {
|
||||
static::forceDeleting(function ($application) {
|
||||
$application->update(['fqdn' => null]);
|
||||
$application->settings()->delete();
|
||||
$storages = $application->persistentStorages()->get();
|
||||
$server = data_get($application, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$application->persistentStorages()->delete();
|
||||
$application->environment_variables()->delete();
|
||||
$application->environment_variables_preview()->delete();
|
||||
@@ -57,19 +166,108 @@ class Application extends BaseModel
|
||||
$task->delete();
|
||||
}
|
||||
$application->tags()->detach();
|
||||
$application->previews()->delete();
|
||||
foreach ($application->deployment_queue as $deployment) {
|
||||
$deployment->delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public function getContainersToStop(bool $previewDeployments = false): array
|
||||
{
|
||||
$containers = $previewDeployments
|
||||
? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
|
||||
: getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
|
||||
|
||||
return $containers->pluck('Names')->toArray();
|
||||
}
|
||||
|
||||
public function stopContainers(array $containerNames, $server, int $timeout = 600)
|
||||
{
|
||||
$processes = [];
|
||||
foreach ($containerNames as $containerName) {
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
|
||||
}
|
||||
|
||||
$startTime = time();
|
||||
while (count($processes) > 0) {
|
||||
$finishedProcesses = array_filter($processes, function ($process) {
|
||||
return ! $process->running();
|
||||
});
|
||||
foreach ($finishedProcesses as $containerName => $process) {
|
||||
unset($processes[$containerName]);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
public function removeContainer(string $containerName, $server)
|
||||
{
|
||||
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
public function forceStopRemainingContainers(array $containerNames, $server)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$workdir = $this->workdir();
|
||||
if (str($workdir)->endsWith($this->uuid)) {
|
||||
ray('Deleting workdir');
|
||||
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(?Collection $persistentStorages)
|
||||
{
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$server = data_get($this, 'destination.server');
|
||||
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
|
||||
} else {
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_connected_networks($uuid)
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
}
|
||||
|
||||
public function additional_servers()
|
||||
{
|
||||
return $this->belongsToMany(Server::class, 'additional_destinations')
|
||||
@@ -131,12 +329,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();
|
||||
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;
|
||||
@@ -174,12 +384,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}";
|
||||
}
|
||||
|
||||
@@ -228,18 +446,13 @@ class Application extends BaseModel
|
||||
|
||||
public function gitCommitLink($link): string
|
||||
{
|
||||
if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
|
||||
if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) {
|
||||
if (str($this->source->html_url)->contains('bitbucket')) {
|
||||
return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
|
||||
}
|
||||
|
||||
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
|
||||
}
|
||||
if (strpos($this->git_repository, 'git@') === 0) {
|
||||
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
|
||||
|
||||
return "https://{$git_repository}/commit/{$link}";
|
||||
}
|
||||
if (str($this->git_repository)->contains('bitbucket')) {
|
||||
$git_repository = str_replace('.git', '', $this->git_repository);
|
||||
$url = Url::fromString($git_repository);
|
||||
@@ -248,6 +461,14 @@ class Application extends BaseModel
|
||||
|
||||
return $url->__toString();
|
||||
}
|
||||
if (strpos($this->git_repository, 'git@') === 0) {
|
||||
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
|
||||
if (data_get($this, 'source.html_url')) {
|
||||
return "{$this->source->html_url}/{$git_repository}/commit/{$link}";
|
||||
}
|
||||
|
||||
return "{$git_repository}/commit/{$link}";
|
||||
}
|
||||
|
||||
return $this->git_repository;
|
||||
}
|
||||
@@ -286,23 +507,6 @@ class Application extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function dockerComposePrLocation(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (is_null($value) || $value === '') {
|
||||
return '/docker-compose.yaml';
|
||||
} else {
|
||||
if ($value !== '/') {
|
||||
return Str::start(Str::replaceEnd('/', '', $value), '/');
|
||||
}
|
||||
|
||||
return Str::start($value, '/');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function baseDirectory(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -327,6 +531,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');
|
||||
@@ -337,6 +546,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 ($server_status !== 'running') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $main_server_status;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function status(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -407,6 +638,14 @@ class Application extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function customNginxConfiguration(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn ($value) => base64_encode($value),
|
||||
get: fn ($value) => base64_decode($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function portsExposesArray(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -510,6 +749,11 @@ class Application extends BaseModel
|
||||
return $this->hasMany(ApplicationPreview::class);
|
||||
}
|
||||
|
||||
public function deployment_queue()
|
||||
{
|
||||
return $this->hasMany(ApplicationDeploymentQueue::class);
|
||||
}
|
||||
|
||||
public function destination()
|
||||
{
|
||||
return $this->morphTo();
|
||||
@@ -532,7 +776,7 @@ class Application extends BaseModel
|
||||
|
||||
public function get_last_successful_deployment()
|
||||
{
|
||||
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
|
||||
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
|
||||
}
|
||||
|
||||
public function get_last_days_deployments()
|
||||
@@ -632,7 +876,7 @@ class Application extends BaseModel
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
|
||||
$newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration);
|
||||
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
} else {
|
||||
@@ -662,21 +906,7 @@ class Application extends BaseModel
|
||||
|
||||
public function customRepository()
|
||||
{
|
||||
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
|
||||
$port = 22;
|
||||
if (count($matches) === 1) {
|
||||
$port = $matches[0];
|
||||
$gitHost = str($this->git_repository)->before(':');
|
||||
$gitRepo = str($this->git_repository)->after('/');
|
||||
$repository = "$gitHost:$gitRepo";
|
||||
} else {
|
||||
$repository = $this->git_repository;
|
||||
}
|
||||
|
||||
return [
|
||||
'repository' => $repository,
|
||||
'port' => $port,
|
||||
];
|
||||
return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source);
|
||||
}
|
||||
|
||||
public function generateBaseDir(string $uuid)
|
||||
@@ -684,6 +914,11 @@ class Application extends BaseModel
|
||||
return "/artifacts/{$uuid}";
|
||||
}
|
||||
|
||||
public function dirOnServer()
|
||||
{
|
||||
return application_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
|
||||
{
|
||||
$baseDir = $this->generateBaseDir($deployment_uuid);
|
||||
@@ -838,7 +1073,7 @@ class Application extends BaseModel
|
||||
$source_html_url_host = $url['host'];
|
||||
$source_html_url_scheme = $url['scheme'];
|
||||
|
||||
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($this->source->getMorphClass() === \App\Models\GithubApp::class) {
|
||||
if ($this->source->is_public) {
|
||||
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
|
||||
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
|
||||
@@ -921,7 +1156,7 @@ class Application extends BaseModel
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
|
||||
} elseif ($git_type === 'github') {
|
||||
} elseif ($git_type === 'github' || $git_type === 'gitea') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
@@ -965,7 +1200,7 @@ class Application extends BaseModel
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
|
||||
} elseif ($git_type === 'github') {
|
||||
} elseif ($git_type === 'github' || $git_type === 'gitea') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
@@ -997,7 +1232,7 @@ class Application extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function parseRawCompose()
|
||||
public function oldRawParser()
|
||||
{
|
||||
try {
|
||||
$yaml = Yaml::parse($this->docker_compose_raw);
|
||||
@@ -1005,6 +1240,7 @@ class Application extends BaseModel
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
$services = data_get($yaml, 'services');
|
||||
|
||||
$commands = collect([]);
|
||||
$services = collect($services)->map(function ($service) use ($commands) {
|
||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||
@@ -1014,9 +1250,9 @@ class Application extends BaseModel
|
||||
$type = null;
|
||||
$source = null;
|
||||
if (is_string($volume)) {
|
||||
$source = Str::of($volume)->before(':');
|
||||
$source = str($volume)->before(':');
|
||||
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
|
||||
$type = Str::of('bind');
|
||||
$type = str('bind');
|
||||
}
|
||||
} elseif (is_array($volume)) {
|
||||
$type = data_get_str($volume, 'type');
|
||||
@@ -1057,9 +1293,11 @@ class Application extends BaseModel
|
||||
instant_remote_process($commands, $this->destination->server, false);
|
||||
}
|
||||
|
||||
public function parseCompose(int $pull_request_id = 0, ?int $preview_id = null)
|
||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
||||
{
|
||||
if ($this->docker_compose_raw) {
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return newParser($this, $pull_request_id, $preview_id);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||
} else {
|
||||
return collect([]);
|
||||
@@ -1072,16 +1310,11 @@ class Application extends BaseModel
|
||||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
$uuid = new Cuid2();
|
||||
$uuid = new Cuid2;
|
||||
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
|
||||
$workdir = rtrim($this->base_directory, '/');
|
||||
$composeFile = $this->docker_compose_location;
|
||||
// $prComposeFile = $this->docker_compose_pr_location;
|
||||
$fileList = collect([".$workdir$composeFile"]);
|
||||
// if ($composeFile !== $prComposeFile) {
|
||||
// $fileList->push(".$prComposeFile");
|
||||
// }
|
||||
|
||||
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
|
||||
if (! $gitRemoteStatus['is_accessible']) {
|
||||
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
|
||||
@@ -1097,46 +1330,54 @@ class Application extends BaseModel
|
||||
'git read-tree -mu HEAD',
|
||||
"cat .$workdir$composeFile",
|
||||
]);
|
||||
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
|
||||
if (! $composeFileContent) {
|
||||
try {
|
||||
$composeFileContent = instant_remote_process($commands, $this->destination->server);
|
||||
} catch (\Exception $e) {
|
||||
if (str($e->getMessage())->contains('No such file')) {
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
if (str($e->getMessage())->contains('fatal: repository') && str($e->getMessage())->contains('does not exist')) {
|
||||
if ($this->deploymentType() === 'deploy_key') {
|
||||
throw new \RuntimeException('Your deploy key does not have access to the repository. Please check your deploy key and try again.');
|
||||
}
|
||||
throw new \RuntimeException('Repository does not exist. Please check your repository URL and try again.');
|
||||
}
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
} finally {
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->save();
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
]);
|
||||
instant_remote_process($commands, $this->destination->server, false);
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
} else {
|
||||
}
|
||||
if ($composeFileContent) {
|
||||
$this->docker_compose_raw = $composeFileContent;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
]);
|
||||
instant_remote_process($commands, $this->destination->server, false);
|
||||
$parsedServices = $this->parseCompose();
|
||||
if ($this->docker_compose_domains) {
|
||||
$json = collect(json_decode($this->docker_compose_domains));
|
||||
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
|
||||
$jsonNames = $json->keys()->toArray();
|
||||
$diff = array_diff($jsonNames, $names);
|
||||
$json = $json->filter(function ($value, $key) use ($diff) {
|
||||
return ! in_array($key, $diff);
|
||||
});
|
||||
if ($json) {
|
||||
$this->docker_compose_domains = json_encode($json);
|
||||
} else {
|
||||
$this->docker_compose_domains = null;
|
||||
$parsedServices = $this->parse();
|
||||
if ($this->docker_compose_domains) {
|
||||
$json = collect(json_decode($this->docker_compose_domains));
|
||||
$names = collect(data_get($parsedServices, 'services'))->keys()->toArray();
|
||||
$jsonNames = $json->keys()->toArray();
|
||||
$diff = array_diff($jsonNames, $names);
|
||||
$json = $json->filter(function ($value, $key) use ($diff) {
|
||||
return ! in_array($key, $diff);
|
||||
});
|
||||
if ($json) {
|
||||
$this->docker_compose_domains = json_encode($json);
|
||||
} else {
|
||||
$this->docker_compose_domains = null;
|
||||
}
|
||||
$this->save();
|
||||
}
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return [
|
||||
'parsedServices' => $parsedServices,
|
||||
'initialDockerComposeLocation' => $this->docker_compose_location,
|
||||
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
|
||||
];
|
||||
return [
|
||||
'parsedServices' => $parsedServices,
|
||||
'initialDockerComposeLocation' => $this->docker_compose_location,
|
||||
];
|
||||
} else {
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
}
|
||||
|
||||
public function parseContainerLabels(?ApplicationPreview $preview = null)
|
||||
@@ -1146,13 +1387,11 @@ class Application extends BaseModel
|
||||
return;
|
||||
}
|
||||
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
|
||||
ray('custom_labels is not base64 encoded');
|
||||
$this->custom_labels = str($customLabels)->replace(',', "\n");
|
||||
$this->custom_labels = base64_encode($customLabels);
|
||||
}
|
||||
$customLabels = base64_decode($this->custom_labels);
|
||||
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
ray('custom_labels contains non-ascii characters');
|
||||
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
|
||||
}
|
||||
$this->custom_labels = base64_encode($customLabels);
|
||||
@@ -1277,7 +1516,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);
|
||||
@@ -1288,4 +1527,126 @@ class Application extends BaseModel
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
public static function getDomainsByUuid(string $uuid): array
|
||||
{
|
||||
$application = self::where('uuid', $uuid)->first();
|
||||
|
||||
if ($application) {
|
||||
return $application->fqdns;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function generateConfig($is_json = false)
|
||||
{
|
||||
$config = collect([]);
|
||||
if ($this->build_pack = 'nixpacks') {
|
||||
$config = collect([
|
||||
'build_pack' => 'nixpacks',
|
||||
'docker_registry_image_name' => $this->docker_registry_image_name,
|
||||
'docker_registry_image_tag' => $this->docker_registry_image_tag,
|
||||
'install_command' => $this->install_command,
|
||||
'build_command' => $this->build_command,
|
||||
'start_command' => $this->start_command,
|
||||
'base_directory' => $this->base_directory,
|
||||
'publish_directory' => $this->publish_directory,
|
||||
'custom_docker_run_options' => $this->custom_docker_run_options,
|
||||
'ports_exposes' => $this->ports_exposes,
|
||||
'ports_mappings' => $this->ports_mapping,
|
||||
'settings' => collect([
|
||||
'is_static' => $this->settings->is_static,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
$config = $config->filter(function ($value) {
|
||||
return str($value)->isNotEmpty();
|
||||
});
|
||||
if ($is_json) {
|
||||
return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function setConfig($config)
|
||||
{
|
||||
$validator = Validator::make(['config' => $config], [
|
||||
'config' => 'required|json',
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
throw new \Exception('Invalid JSON format');
|
||||
}
|
||||
$config = json_decode($config, true);
|
||||
|
||||
$deepValidator = Validator::make(['config' => $config], [
|
||||
'config.build_pack' => 'required|string',
|
||||
'config.base_directory' => 'required|string',
|
||||
'config.publish_directory' => 'required|string',
|
||||
'config.ports_exposes' => 'required|string',
|
||||
'config.settings.is_static' => 'required|boolean',
|
||||
]);
|
||||
if ($deepValidator->fails()) {
|
||||
throw new \Exception('Invalid data');
|
||||
}
|
||||
$config = $deepValidator->validated()['config'];
|
||||
|
||||
try {
|
||||
$settings = data_get($config, 'settings', []);
|
||||
data_forget($config, 'settings');
|
||||
$this->update($config);
|
||||
$this->settings()->update($settings);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception('Failed to update application settings');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,58 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Project model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'application_id' => ['type' => 'string'],
|
||||
'deployment_uuid' => ['type' => 'string'],
|
||||
'pull_request_id' => ['type' => 'integer'],
|
||||
'force_rebuild' => ['type' => 'boolean'],
|
||||
'commit' => ['type' => 'string'],
|
||||
'status' => ['type' => 'string'],
|
||||
'is_webhook' => ['type' => 'boolean'],
|
||||
'is_api' => ['type' => 'boolean'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'updated_at' => ['type' => 'string'],
|
||||
'logs' => ['type' => 'string'],
|
||||
'current_process_id' => ['type' => 'string'],
|
||||
'restart_only' => ['type' => 'boolean'],
|
||||
'git_type' => ['type' => 'string'],
|
||||
'server_id' => ['type' => 'integer'],
|
||||
'application_name' => ['type' => 'string'],
|
||||
'server_name' => ['type' => 'string'],
|
||||
'deployment_url' => ['type' => 'string'],
|
||||
'destination_id' => ['type' => 'string'],
|
||||
'only_this_server' => ['type' => 'boolean'],
|
||||
'rollback' => ['type' => 'boolean'],
|
||||
'commit_message' => ['type' => 'string'],
|
||||
],
|
||||
)]
|
||||
class ApplicationDeploymentQueue extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function application(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => Application::find($this->application_id),
|
||||
);
|
||||
}
|
||||
|
||||
public function server(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => Server::find($this->server_id),
|
||||
);
|
||||
}
|
||||
|
||||
public function setStatus(string $status)
|
||||
{
|
||||
$this->update([
|
||||
|
||||
@@ -12,9 +12,9 @@ class ApplicationPreview extends BaseModel
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($preview) {
|
||||
if ($preview->application->build_pack === 'dockercompose') {
|
||||
if (data_get($preview, 'application.build_pack') === 'dockercompose') {
|
||||
$server = $preview->application->destination->server;
|
||||
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
|
||||
$composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
|
||||
$volumes = data_get($composeFile, 'volumes');
|
||||
$networks = data_get($composeFile, 'networks');
|
||||
$networkKeys = collect($networks)->keys();
|
||||
@@ -28,6 +28,11 @@ class ApplicationPreview extends BaseModel
|
||||
});
|
||||
}
|
||||
});
|
||||
static::saving(function ($preview) {
|
||||
if ($preview->isDirty('status')) {
|
||||
$preview->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
|
||||
@@ -35,6 +40,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 +59,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,20 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Environment model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'name' => ['type' => 'string'],
|
||||
'project_id' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'updated_at' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
]
|
||||
)]
|
||||
class Environment extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
@@ -14,10 +27,8 @@ class Environment extends Model
|
||||
static::deleting(function ($environment) {
|
||||
$shared_variables = $environment->environment_variables();
|
||||
foreach ($shared_variables as $shared_variable) {
|
||||
ray('Deleting environment shared variable: '.$shared_variable->name);
|
||||
$shared_variable->delete();
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +38,9 @@ class Environment extends Model
|
||||
$this->redis()->count() == 0 &&
|
||||
$this->postgresqls()->count() == 0 &&
|
||||
$this->mysqls()->count() == 0 &&
|
||||
$this->keydbs()->count() == 0 &&
|
||||
$this->dragonflies()->count() == 0 &&
|
||||
$this->clickhouses()->count() == 0 &&
|
||||
$this->mariadbs()->count() == 0 &&
|
||||
$this->mongodbs()->count() == 0 &&
|
||||
$this->services()->count() == 0;
|
||||
@@ -109,7 +123,7 @@ class Environment extends Model
|
||||
protected function name(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => strtolower($value),
|
||||
set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,32 @@ 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 Symfony\Component\Yaml\Yaml;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Environment Variable model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'application_id' => ['type' => 'integer'],
|
||||
'service_id' => ['type' => 'integer'],
|
||||
'database_id' => ['type' => 'integer'],
|
||||
'is_build_time' => ['type' => 'boolean'],
|
||||
'is_literal' => ['type' => 'boolean'],
|
||||
'is_multiline' => ['type' => 'boolean'],
|
||||
'is_preview' => ['type' => 'boolean'],
|
||||
'is_shared' => ['type' => 'boolean'],
|
||||
'is_shown_once' => ['type' => 'boolean'],
|
||||
'key' => ['type' => 'string'],
|
||||
'value' => ['type' => 'string'],
|
||||
'real_value' => ['type' => 'string'],
|
||||
'version' => ['type' => 'string'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'updated_at' => ['type' => 'string'],
|
||||
]
|
||||
)]
|
||||
class EnvironmentVariable extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
@@ -21,10 +44,15 @@ class EnvironmentVariable extends Model
|
||||
'version' => 'string',
|
||||
];
|
||||
|
||||
protected $appends = ['real_value', 'is_shared'];
|
||||
protected $appends = ['real_value', 'is_shared', 'is_really_required'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function (Model $model) {
|
||||
if (! $model->uuid) {
|
||||
$model->uuid = (string) new Cuid2;
|
||||
}
|
||||
});
|
||||
static::created(function (EnvironmentVariable $environment_variable) {
|
||||
if ($environment_variable->application_id && ! $environment_variable->is_preview) {
|
||||
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
|
||||
@@ -46,6 +74,9 @@ class EnvironmentVariable extends Model
|
||||
'version' => config('version'),
|
||||
]);
|
||||
});
|
||||
static::saving(function (EnvironmentVariable $environmentVariable) {
|
||||
$environmentVariable->updateIsShared();
|
||||
});
|
||||
}
|
||||
|
||||
public function service()
|
||||
@@ -68,8 +99,22 @@ class EnvironmentVariable extends Model
|
||||
$resource = Application::find($this->application_id);
|
||||
} elseif ($this->service_id) {
|
||||
$resource = Service::find($this->service_id);
|
||||
} elseif ($this->database_id) {
|
||||
$resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id'));
|
||||
} elseif ($this->standalone_postgresql_id) {
|
||||
$resource = StandalonePostgresql::find($this->standalone_postgresql_id);
|
||||
} elseif ($this->standalone_redis_id) {
|
||||
$resource = StandaloneRedis::find($this->standalone_redis_id);
|
||||
} elseif ($this->standalone_mongodb_id) {
|
||||
$resource = StandaloneMongodb::find($this->standalone_mongodb_id);
|
||||
} elseif ($this->standalone_mysql_id) {
|
||||
$resource = StandaloneMysql::find($this->standalone_mysql_id);
|
||||
} elseif ($this->standalone_mariadb_id) {
|
||||
$resource = StandaloneMariadb::find($this->standalone_mariadb_id);
|
||||
} elseif ($this->standalone_keydb_id) {
|
||||
$resource = StandaloneKeydb::find($this->standalone_keydb_id);
|
||||
} elseif ($this->standalone_dragonfly_id) {
|
||||
$resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
|
||||
} elseif ($this->standalone_clickhouse_id) {
|
||||
$resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
@@ -84,69 +129,14 @@ class EnvironmentVariable extends Model
|
||||
$env = $this->get_real_environment_variables($this->value, $resource);
|
||||
|
||||
return data_get($env, 'value', $env);
|
||||
if (is_string($env)) {
|
||||
return $env;
|
||||
}
|
||||
|
||||
return $env->value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function isFoundInCompose(): Attribute
|
||||
protected function isReallyRequired(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (! $this->application_id) {
|
||||
return true;
|
||||
}
|
||||
$found_in_compose = false;
|
||||
$found_in_args = false;
|
||||
$resource = $this->resource();
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
if (! $compose) {
|
||||
return true;
|
||||
}
|
||||
$yaml = Yaml::parse($compose);
|
||||
$services = collect(data_get($yaml, 'services'));
|
||||
if ($services->isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
foreach ($services as $service) {
|
||||
$environments = collect(data_get($service, 'environment'));
|
||||
$args = collect(data_get($service, 'build.args'));
|
||||
if ($environments->isEmpty() && $args->isEmpty()) {
|
||||
$found_in_compose = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$found_in_compose = $environments->contains(function ($item) {
|
||||
if (str($item)->contains('=')) {
|
||||
$item = str($item)->before('=');
|
||||
}
|
||||
|
||||
return strpos($item, $this->key) !== false;
|
||||
});
|
||||
|
||||
if ($found_in_compose) {
|
||||
break;
|
||||
}
|
||||
|
||||
$found_in_args = $args->contains(function ($item) {
|
||||
if (str($item)->contains('=')) {
|
||||
$item = str($item)->before('=');
|
||||
}
|
||||
|
||||
return strpos($item, $this->key) !== false;
|
||||
});
|
||||
|
||||
if ($found_in_args) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $found_in_compose || $found_in_args;
|
||||
}
|
||||
get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,32 +156,38 @@ class EnvironmentVariable extends Model
|
||||
|
||||
private function get_real_environment_variables(?string $environment_variable = null, $resource = null)
|
||||
{
|
||||
if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) {
|
||||
if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) {
|
||||
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::of($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
|
||||
@@ -205,7 +201,7 @@ class EnvironmentVariable extends Model
|
||||
|
||||
private function set_environment_variables(?string $environment_variable = null): ?string
|
||||
{
|
||||
if (is_null($environment_variable) && $environment_variable == '') {
|
||||
if (is_null($environment_variable) && $environment_variable === '') {
|
||||
return null;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
@@ -220,7 +216,14 @@ class EnvironmentVariable extends Model
|
||||
protected function key(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => Str::of($value)->trim(),
|
||||
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateIsShared(): void
|
||||
{
|
||||
$type = str($this->value)->after('{{')->before('.')->value;
|
||||
$isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
|
||||
$this->is_shared = $isShared;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,22 @@ class GithubApp extends BaseModel
|
||||
'webhook_secret',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::deleting(function (GithubApp $github_app) {
|
||||
$applications_count = Application::where('source_id', $github_app->id)->count();
|
||||
if ($applications_count > 0) {
|
||||
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
|
||||
}
|
||||
$github_app->privateKey()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return GithubApp::whereTeamId(currentTeam()->id);
|
||||
}
|
||||
|
||||
public static function public()
|
||||
{
|
||||
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
|
||||
@@ -30,15 +46,9 @@ class GithubApp extends BaseModel
|
||||
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
public function team()
|
||||
{
|
||||
static::deleting(function (GithubApp $github_app) {
|
||||
$applications_count = Application::where('source_id', $github_app->id)->count();
|
||||
if ($applications_count > 0) {
|
||||
throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.');
|
||||
}
|
||||
$github_app->privateKey()->delete();
|
||||
});
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
|
||||
public function applications()
|
||||
@@ -55,7 +65,7 @@ class GithubApp extends BaseModel
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if ($this->getMorphClass() === 'App\Models\GithubApp') {
|
||||
if ($this->getMorphClass() === \App\Models\GithubApp::class) {
|
||||
return 'github';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,11 @@ class GitlabApp extends BaseModel
|
||||
'app_secret',
|
||||
];
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return GitlabApp::whereTeamId(currentTeam()->id);
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return $this->morphMany(Application::class, 'source');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -17,8 +18,26 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
protected $casts = [
|
||||
'resale_license' => 'encrypted',
|
||||
'smtp_password' => 'encrypted',
|
||||
'allowed_ip_ranges' => 'array',
|
||||
'is_auto_update_enabled' => 'boolean',
|
||||
'auto_update_frequency' => 'string',
|
||||
'update_check_frequency' => 'string',
|
||||
'sentinel_token' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updated(function ($settings) {
|
||||
if ($settings->isDirty('helper_version')) {
|
||||
Server::chunkById(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function fqdn(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -33,6 +52,30 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
);
|
||||
}
|
||||
|
||||
public function updateCheckFrequency(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
},
|
||||
get: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function autoUpdateFrequency(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
},
|
||||
get: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static function get()
|
||||
{
|
||||
return InstanceSettings::findOrFail(0);
|
||||
@@ -47,4 +90,27 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
|
||||
return explode(',', $recipients);
|
||||
}
|
||||
|
||||
public function getTitleDisplayName(): string
|
||||
{
|
||||
$instanceName = $this->instance_name;
|
||||
if (! $instanceName) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return "[{$instanceName}]";
|
||||
}
|
||||
|
||||
// public function helperVersion(): Attribute
|
||||
// {
|
||||
// return Attribute::make(
|
||||
// get: function ($value) {
|
||||
// if (isDev()) {
|
||||
// return 'latest';
|
||||
// }
|
||||
|
||||
// return $value;
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
class Kubernetes extends BaseModel
|
||||
{
|
||||
}
|
||||
class Kubernetes extends BaseModel {}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Events\FileStorageChanged;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class LocalFileVolume extends BaseModel
|
||||
@@ -23,8 +24,9 @@ class LocalFileVolume extends BaseModel
|
||||
return $this->morphTo('resource');
|
||||
}
|
||||
|
||||
public function deleteStorageOnServer()
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
$workdir = $this->resource->service->workdir();
|
||||
@@ -33,20 +35,56 @@ class LocalFileVolume extends BaseModel
|
||||
$workdir = $this->resource->workdir();
|
||||
$server = $this->resource->destination->server;
|
||||
}
|
||||
$commands = collect([
|
||||
"cd $workdir",
|
||||
]);
|
||||
$fs_path = data_get($this, 'fs_path');
|
||||
if ($fs_path && $fs_path != '/' && $fs_path != '.' && $fs_path != '..') {
|
||||
$commands->push("rm -rf $fs_path");
|
||||
$commands = collect([]);
|
||||
$path = data_get_str($this, 'fs_path');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
ray($commands);
|
||||
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
|
||||
if ($isFile === 'OK') {
|
||||
$content = instant_remote_process(["cat $path"], $server, false);
|
||||
$this->content = $content;
|
||||
$this->is_directory = false;
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
return instant_remote_process($commands, $server);
|
||||
public function deleteStorageOnServer()
|
||||
{
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
$workdir = $this->resource->service->workdir();
|
||||
$server = $this->resource->service->server;
|
||||
} else {
|
||||
$workdir = $this->resource->workdir();
|
||||
$server = $this->resource->destination->server;
|
||||
}
|
||||
$commands = collect([]);
|
||||
$path = data_get_str($this, 'fs_path');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
$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 ($path && $path != '/' && $path != '.' && $path != '..') {
|
||||
if ($isFile === 'OK') {
|
||||
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
|
||||
} elseif ($isDir === 'OK') {
|
||||
$commands->push("rm -rf $path > /dev/null 2>&1 || true");
|
||||
$commands->push("rmdir $path > /dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
if ($commands->count() > 0) {
|
||||
return instant_remote_process($commands, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function saveStorageOnServer()
|
||||
{
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
$workdir = $this->resource->service->workdir();
|
||||
@@ -55,13 +93,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('/');
|
||||
@@ -69,35 +104,50 @@ class LocalFileVolume extends BaseModel
|
||||
$commands->push("mkdir -p $parent_dir > /dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
$fileVolume = $this;
|
||||
$path = str(data_get($fileVolume, 'fs_path'));
|
||||
$content = data_get($fileVolume, 'content');
|
||||
$path = data_get_str($this, 'fs_path');
|
||||
$content = data_get($this, 'content');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
}
|
||||
$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) {
|
||||
if ($isFile === 'OK' && $this->is_directory) {
|
||||
$content = instant_remote_process(["cat $path"], $server, false);
|
||||
$this->is_directory = false;
|
||||
$this->content = $content;
|
||||
$this->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) {
|
||||
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.');
|
||||
} elseif ($isDir === 'OK' && ! $this->is_directory) {
|
||||
if ($path === '/' || $path === '.' || $path === '..' || $path === '' || str($path)->isEmpty() || is_null($path)) {
|
||||
$this->is_directory = true;
|
||||
$this->save();
|
||||
throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file. <br><br>Please delete the directory on the server or mark it as directory.');
|
||||
}
|
||||
instant_remote_process([
|
||||
"rm -fr $path",
|
||||
"touch $path",
|
||||
], $server, false);
|
||||
FileStorageChanged::dispatch(data_get($server, 'team_id'));
|
||||
}
|
||||
if (! $fileVolume->is_directory && $isDir == 'NOK') {
|
||||
if ($isDir === 'NOK' && ! $this->is_directory) {
|
||||
$chmod = data_get($this, 'chmod');
|
||||
$chown = data_get($this, 'chown');
|
||||
if ($content) {
|
||||
$content = base64_encode($content);
|
||||
$chmod = $fileVolume->chmod;
|
||||
$chown = $fileVolume->chown;
|
||||
$commands->push("echo '$content' | base64 -d | tee $path > /dev/null");
|
||||
$commands->push("chmod +x $path");
|
||||
if ($chown) {
|
||||
$commands->push("chown $chown $path");
|
||||
}
|
||||
if ($chmod) {
|
||||
$commands->push("chmod $chmod $path");
|
||||
}
|
||||
} else {
|
||||
$commands->push("touch $path");
|
||||
}
|
||||
} elseif ($isDir == 'NOK' && $fileVolume->is_directory) {
|
||||
$commands->push("chmod +x $path");
|
||||
if ($chown) {
|
||||
$commands->push("chown $chown $path");
|
||||
}
|
||||
if ($chmod) {
|
||||
$commands->push("chmod $chmod $path");
|
||||
}
|
||||
} elseif ($isDir === 'NOK' && $this->is_directory) {
|
||||
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LocalPersistentVolume extends Model
|
||||
{
|
||||
@@ -33,14 +32,14 @@ class LocalPersistentVolume extends Model
|
||||
protected function name(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => Str::of($value)->trim()->value,
|
||||
set: fn (string $value) => str($value)->trim()->value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function mountPath(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => Str::of($value)->trim()->start('/')->value
|
||||
set: fn (string $value) => str($value)->trim()->start('/')->value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +48,7 @@ class LocalPersistentVolume extends Model
|
||||
return Attribute::make(
|
||||
set: function (?string $value) {
|
||||
if ($value) {
|
||||
return Str::of($value)->trim()->start('/')->value;
|
||||
return str($value)->trim()->start('/')->value;
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
|
||||
@@ -2,41 +2,167 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use OpenApi\Attributes as OA;
|
||||
use phpseclib3\Crypt\PublicKeyLoader;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Private Key model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'private_key' => ['type' => 'string', 'format' => 'private-key'],
|
||||
'is_git_related' => ['type' => 'boolean'],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'updated_at' => ['type' => 'string'],
|
||||
],
|
||||
)]
|
||||
class PrivateKey extends BaseModel
|
||||
{
|
||||
use WithRateLimiting;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'description',
|
||||
'private_key',
|
||||
'is_git_related',
|
||||
'team_id',
|
||||
'fingerprint',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'private_key' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saving(function ($key) {
|
||||
$key->private_key = formatPrivateKey($key->private_key);
|
||||
|
||||
if (! self::validatePrivateKey($key->private_key)) {
|
||||
throw ValidationException::withMessages([
|
||||
'private_key' => ['The private key is invalid.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$key->fingerprint = self::generateFingerprint($key->private_key);
|
||||
if (self::fingerprintExists($key->fingerprint, $key->id)) {
|
||||
throw ValidationException::withMessages([
|
||||
'private_key' => ['This private key already exists.'],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function ($key) {
|
||||
self::deleteFromStorage($key);
|
||||
});
|
||||
}
|
||||
|
||||
public function getPublicKey()
|
||||
{
|
||||
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam(array $select = ['*'])
|
||||
{
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
|
||||
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||
return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
|
||||
}
|
||||
|
||||
public function publicKey()
|
||||
public static function validatePrivateKey($privateKey)
|
||||
{
|
||||
try {
|
||||
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||
PublicKeyLoader::load($privateKey);
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return 'Error loading private key';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function isEmpty()
|
||||
public static function createAndStore(array $data)
|
||||
{
|
||||
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
|
||||
return true;
|
||||
}
|
||||
$privateKey = new self($data);
|
||||
$privateKey->save();
|
||||
$privateKey->storeInFileSystem();
|
||||
|
||||
return false;
|
||||
return $privateKey;
|
||||
}
|
||||
|
||||
public static function generateNewKeyPair($type = 'rsa')
|
||||
{
|
||||
try {
|
||||
$instance = new self;
|
||||
$instance->rateLimit(10);
|
||||
$name = generate_random_name();
|
||||
$description = 'Created by Coolify';
|
||||
$keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'private_key' => $keyPair['private'],
|
||||
'public_key' => $keyPair['public'],
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static function extractPublicKeyFromPrivate($privateKey)
|
||||
{
|
||||
try {
|
||||
$key = PublicKeyLoader::load($privateKey);
|
||||
|
||||
return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateAndExtractPublicKey($privateKey)
|
||||
{
|
||||
$isValid = self::validatePrivateKey($privateKey);
|
||||
$publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
|
||||
|
||||
return [
|
||||
'isValid' => $isValid,
|
||||
'publicKey' => $publicKey,
|
||||
];
|
||||
}
|
||||
|
||||
public function storeInFileSystem()
|
||||
{
|
||||
$filename = "ssh_key@{$this->uuid}";
|
||||
Storage::disk('ssh-keys')->put($filename, $this->private_key);
|
||||
|
||||
return "/var/www/html/storage/app/ssh/keys/{$filename}";
|
||||
}
|
||||
|
||||
public static function deleteFromStorage(self $privateKey)
|
||||
{
|
||||
$filename = "ssh_key@{$privateKey->uuid}";
|
||||
Storage::disk('ssh-keys')->delete($filename);
|
||||
}
|
||||
|
||||
public function getKeyLocation()
|
||||
{
|
||||
return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
|
||||
}
|
||||
|
||||
public function updatePrivateKey(array $data)
|
||||
{
|
||||
$this->update($data);
|
||||
$this->storeInFileSystem();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function servers()
|
||||
@@ -58,4 +184,53 @@ class PrivateKey extends BaseModel
|
||||
{
|
||||
return $this->hasMany(GitlabApp::class);
|
||||
}
|
||||
|
||||
public function isInUse()
|
||||
{
|
||||
return $this->servers()->exists()
|
||||
|| $this->applications()->exists()
|
||||
|| $this->githubApps()->exists()
|
||||
|| $this->gitlabApps()->exists();
|
||||
}
|
||||
|
||||
public function safeDelete()
|
||||
{
|
||||
if (! $this->isInUse()) {
|
||||
$this->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function generateFingerprint($privateKey)
|
||||
{
|
||||
try {
|
||||
$key = PublicKeyLoader::load($privateKey);
|
||||
$publicKey = $key->getPublicKey();
|
||||
|
||||
return $publicKey->getFingerprint('sha256');
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static function fingerprintExists($fingerprint, $excludeId = null)
|
||||
{
|
||||
$query = self::where('fingerprint', $fingerprint);
|
||||
|
||||
if (! is_null($excludeId)) {
|
||||
$query->where('id', '!=', $excludeId);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
public static function cleanupUnusedKeys()
|
||||
{
|
||||
self::ownedByCurrentTeam()->each(function ($privateKey) {
|
||||
$privateKey->safeDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,33 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Project model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'environments' => new OA\Property(
|
||||
property: 'environments',
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Environment'),
|
||||
description: 'The environments of the project.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class Project extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['default_environment'];
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Project::whereTeamId(currentTeam()->id)->orderBy('name');
|
||||
return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
@@ -27,7 +47,6 @@ class Project extends BaseModel
|
||||
$project->settings()->delete();
|
||||
$shared_variables = $project->environment_variables();
|
||||
foreach ($shared_variables as $shared_variable) {
|
||||
ray('Deleting project shared variable: '.$shared_variable->name);
|
||||
$shared_variable->delete();
|
||||
}
|
||||
});
|
||||
@@ -103,13 +122,36 @@ class Project extends BaseModel
|
||||
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
|
||||
}
|
||||
|
||||
public function resource_count()
|
||||
public function isEmpty()
|
||||
{
|
||||
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
|
||||
return $this->applications()->count() == 0 &&
|
||||
$this->redis()->count() == 0 &&
|
||||
$this->postgresqls()->count() == 0 &&
|
||||
$this->mysqls()->count() == 0 &&
|
||||
$this->keydbs()->count() == 0 &&
|
||||
$this->dragonflies()->count() == 0 &&
|
||||
$this->clickhouses()->count() == 0 &&
|
||||
$this->mariadbs()->count() == 0 &&
|
||||
$this->mongodbs()->count() == 0 &&
|
||||
$this->services()->count() == 0;
|
||||
}
|
||||
|
||||
public function databases()
|
||||
{
|
||||
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
|
||||
}
|
||||
|
||||
public function getDefaultEnvironmentAttribute()
|
||||
{
|
||||
$default = $this->environments()->where('name', 'production')->first();
|
||||
if ($default) {
|
||||
return $default->name;
|
||||
}
|
||||
$default = $this->environments()->get();
|
||||
if ($default->count() > 0) {
|
||||
return $default->sortBy('created_at')->first()->name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
|
||||
return "{$this->endpoint}/{$this->bucket}";
|
||||
}
|
||||
|
||||
public function isHetzner()
|
||||
{
|
||||
return str($this->endpoint)->contains('your-objectstorage.com');
|
||||
}
|
||||
|
||||
public function isDigitalOcean()
|
||||
{
|
||||
return str($this->endpoint)->contains('digitaloceanspaces.com');
|
||||
}
|
||||
|
||||
public function testConnection(bool $shouldSave = false)
|
||||
{
|
||||
try {
|
||||
@@ -50,7 +60,7 @@ class S3Storage extends BaseModel
|
||||
} catch (\Throwable $e) {
|
||||
$this->is_usable = false;
|
||||
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
|
||||
$mail = new MailMessage();
|
||||
$mail = new MailMessage;
|
||||
$mail->subject('Coolify: S3 Storage Connection Error');
|
||||
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
|
||||
$users = collect([]);
|
||||
|
||||
@@ -22,7 +22,8 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
|
||||
public function executions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ScheduledDatabaseBackupExecution::class);
|
||||
// Last execution first
|
||||
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function s3()
|
||||
@@ -34,4 +35,22 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
{
|
||||
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if ($this->database) {
|
||||
if ($this->database instanceof ServiceDatabase) {
|
||||
$destination = data_get($this->database->service, 'destination');
|
||||
$server = data_get($destination, 'server');
|
||||
} else {
|
||||
$destination = data_get($this->database, 'destination');
|
||||
$server = data_get($destination, 'server');
|
||||
}
|
||||
if ($server) {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,26 @@ class ScheduledTask extends BaseModel
|
||||
|
||||
public function executions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ScheduledTaskExecution::class);
|
||||
// Last execution first
|
||||
return $this->hasMany(ScheduledTaskExecution::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if ($this->application) {
|
||||
if ($this->application->destination && $this->application->destination->server) {
|
||||
return $this->application->destination->server;
|
||||
}
|
||||
} elseif ($this->service) {
|
||||
if ($this->service->destination && $this->service->destination->server) {
|
||||
return $this->service->destination->server;
|
||||
}
|
||||
} elseif ($this->database) {
|
||||
if ($this->database->destination && $this->database->destination->server) {
|
||||
return $this->database->destination->server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,51 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Server\InstallDocker;
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Notifications\Server\Revived;
|
||||
use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Notifications\Server\Reachable;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Server model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'ip' => ['type' => 'string'],
|
||||
'user' => ['type' => 'string'],
|
||||
'port' => ['type' => 'integer'],
|
||||
'proxy' => ['type' => 'object'],
|
||||
'high_disk_usage_notification_sent' => ['type' => 'boolean'],
|
||||
'unreachable_notification_sent' => ['type' => 'boolean'],
|
||||
'unreachable_count' => ['type' => 'integer'],
|
||||
'validation_logs' => ['type' => 'string'],
|
||||
'log_drain_notification_sent' => ['type' => 'boolean'],
|
||||
'swarm_cluster' => ['type' => 'string'],
|
||||
'delete_unused_volumes' => ['type' => 'boolean'],
|
||||
'delete_unused_networks' => ['type' => 'boolean'],
|
||||
]
|
||||
)]
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use SchemalessAttributesTrait;
|
||||
use SchemalessAttributesTrait, SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
@@ -30,19 +56,56 @@ class Server extends BaseModel
|
||||
static::saving(function ($server) {
|
||||
$payload = [];
|
||||
if ($server->user) {
|
||||
$payload['user'] = Str::of($server->user)->trim();
|
||||
$payload['user'] = str($server->user)->trim();
|
||||
}
|
||||
if ($server->ip) {
|
||||
$payload['ip'] = Str::of($server->ip)->trim();
|
||||
$payload['ip'] = str($server->ip)->trim();
|
||||
}
|
||||
$server->forceFill($payload);
|
||||
});
|
||||
static::saved(function ($server) {
|
||||
if ($server->privateKey?->isDirty()) {
|
||||
refresh_server_connection($server->privateKey);
|
||||
}
|
||||
});
|
||||
static::created(function ($server) {
|
||||
ServerSetting::create([
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
if ($server->id === 0) {
|
||||
if ($server->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($server->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'name' => 'coolify-overlay',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
|
||||
static::forceDeleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
@@ -50,18 +113,38 @@ class Server extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public $casts = [
|
||||
protected $casts = [
|
||||
'proxy' => SchemalessAttributes::class,
|
||||
'logdrain_axiom_api_key' => 'encrypted',
|
||||
'logdrain_newrelic_license_key' => 'encrypted',
|
||||
'delete_unused_volumes' => 'boolean',
|
||||
'delete_unused_networks' => 'boolean',
|
||||
'unreachable_notification_sent' => 'boolean',
|
||||
'is_build_server' => 'boolean',
|
||||
'force_disabled' => 'boolean',
|
||||
];
|
||||
|
||||
protected $schemalessAttributes = [
|
||||
'proxy',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'ip',
|
||||
'port',
|
||||
'user',
|
||||
'description',
|
||||
'private_key_id',
|
||||
'team_id',
|
||||
];
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public function type()
|
||||
{
|
||||
return 'server';
|
||||
}
|
||||
|
||||
public static function isReachable()
|
||||
{
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
|
||||
@@ -94,39 +177,9 @@ class Server extends BaseModel
|
||||
return $this->hasOne(ServerSetting::class);
|
||||
}
|
||||
|
||||
public function addInitialNetwork()
|
||||
public function proxySet()
|
||||
{
|
||||
if ($this->id === 0) {
|
||||
if ($this->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($this->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'name' => 'coolify-overlay',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
|
||||
}
|
||||
|
||||
public function setupDefault404Redirect()
|
||||
@@ -134,13 +187,13 @@ 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') {
|
||||
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
|
||||
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
|
||||
}
|
||||
if (empty($redirect_url)) {
|
||||
if ($proxy_type === 'CADDY') {
|
||||
if ($proxy_type === ProxyTypes::CADDY->value) {
|
||||
$conf = ':80, :443 {
|
||||
respond 404
|
||||
}';
|
||||
@@ -164,7 +217,7 @@ respond 404
|
||||
|
||||
return;
|
||||
}
|
||||
if ($proxy_type === 'TRAEFIK_V2') {
|
||||
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
|
||||
$dynamic_conf = [
|
||||
'http' => [
|
||||
'routers' => [
|
||||
@@ -174,10 +227,13 @@ respond 404
|
||||
1 => 'https',
|
||||
],
|
||||
'service' => 'noop',
|
||||
'rule' => 'HostRegexp(`{catchall:.*}`)',
|
||||
'rule' => 'HostRegexp(`.+`)',
|
||||
'tls' => [
|
||||
'certResolver' => 'letsencrypt',
|
||||
],
|
||||
'priority' => 1,
|
||||
'middlewares' => [
|
||||
0 => 'redirect-regexp@file',
|
||||
0 => 'redirect-regexp',
|
||||
],
|
||||
],
|
||||
],
|
||||
@@ -210,7 +266,7 @@ respond 404
|
||||
$conf;
|
||||
|
||||
$base64 = base64_encode($conf);
|
||||
} elseif ($proxy_type === 'CADDY') {
|
||||
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
|
||||
$conf = ":80, :443 {
|
||||
redir $redirect_url
|
||||
}";
|
||||
@@ -226,9 +282,6 @@ respond 404
|
||||
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
|
||||
], $this);
|
||||
|
||||
if (config('app.env') == 'local') {
|
||||
ray($conf);
|
||||
}
|
||||
if ($proxy_type === 'CADDY') {
|
||||
$this->reloadCaddy();
|
||||
}
|
||||
@@ -236,11 +289,11 @@ respond 404
|
||||
|
||||
public function setupDynamicProxyConfiguration()
|
||||
{
|
||||
$settings = InstanceSettings::get();
|
||||
$settings = instanceSettings();
|
||||
$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)) {
|
||||
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
|
||||
instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $this);
|
||||
@@ -278,6 +331,13 @@ respond 404
|
||||
'service' => 'coolify-realtime',
|
||||
'rule' => "Host(`{$host}`) && PathPrefix(`/app`)",
|
||||
],
|
||||
'coolify-terminal-ws' => [
|
||||
'entryPoints' => [
|
||||
0 => 'http',
|
||||
],
|
||||
'service' => 'coolify-terminal',
|
||||
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
|
||||
],
|
||||
],
|
||||
'services' => [
|
||||
'coolify' => [
|
||||
@@ -298,6 +358,15 @@ respond 404
|
||||
],
|
||||
],
|
||||
],
|
||||
'coolify-terminal' => [
|
||||
'loadBalancer' => [
|
||||
'servers' => [
|
||||
0 => [
|
||||
'url' => 'http://coolify-realtime:6002',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -327,6 +396,16 @@ respond 404
|
||||
'certresolver' => 'letsencrypt',
|
||||
],
|
||||
];
|
||||
$traefik_dynamic_conf['http']['routers']['coolify-terminal-wss'] = [
|
||||
'entryPoints' => [
|
||||
0 => 'https',
|
||||
],
|
||||
'service' => 'coolify-terminal',
|
||||
'rule' => "Host(`{$host}`) && PathPrefix(`/terminal/ws`)",
|
||||
'tls' => [
|
||||
'certresolver' => 'letsencrypt',
|
||||
],
|
||||
];
|
||||
}
|
||||
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
|
||||
$yaml =
|
||||
@@ -340,13 +419,13 @@ respond 404
|
||||
"echo '$base64' | base64 -d | tee $file > /dev/null",
|
||||
], $this);
|
||||
|
||||
if (config('app.env') == 'local') {
|
||||
if (config('app.env') === 'local') {
|
||||
// ray($yaml);
|
||||
}
|
||||
}
|
||||
} elseif ($this->proxyType() === 'CADDY') {
|
||||
$file = "$dynamic_config_path/coolify.caddy";
|
||||
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
|
||||
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
|
||||
instant_remote_process([
|
||||
"rm -f $file",
|
||||
], $this);
|
||||
@@ -360,6 +439,9 @@ $schema://$host {
|
||||
handle /app/* {
|
||||
reverse_proxy coolify-realtime:6001
|
||||
}
|
||||
handle /terminal/ws {
|
||||
reverse_proxy coolify-realtime:6002
|
||||
}
|
||||
reverse_proxy coolify:80
|
||||
}";
|
||||
$base64 = base64_encode($caddy_file);
|
||||
@@ -386,12 +468,20 @@ $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) {
|
||||
$proxy_path = $proxy_path;
|
||||
if ($proxyType === ProxyTypes::TRAEFIK->value) {
|
||||
// Do nothing
|
||||
} elseif ($proxyType === ProxyTypes::CADDY->value) {
|
||||
$proxy_path = $proxy_path.'/caddy';
|
||||
if (isDev()) {
|
||||
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
|
||||
} else {
|
||||
$proxy_path = $proxy_path.'/caddy';
|
||||
}
|
||||
} elseif ($proxyType === ProxyTypes::NGINX->value) {
|
||||
$proxy_path = $proxy_path.'/nginx';
|
||||
if (isDev()) {
|
||||
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
|
||||
} else {
|
||||
$proxy_path = $proxy_path.'/nginx';
|
||||
}
|
||||
}
|
||||
|
||||
return $proxy_path;
|
||||
@@ -399,15 +489,6 @@ $schema://$host {
|
||||
|
||||
public function proxyType()
|
||||
{
|
||||
// $proxyType = $this->proxy->get('type');
|
||||
// if ($proxyType === ProxyTypes::NONE->value) {
|
||||
// return $proxyType;
|
||||
// }
|
||||
// if (is_null($proxyType)) {
|
||||
// $this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
|
||||
// $this->proxy->status = ProxyStatus::EXITED->value;
|
||||
// $this->save();
|
||||
// }
|
||||
return data_get($this->proxy, 'type');
|
||||
}
|
||||
|
||||
@@ -426,20 +507,6 @@ $schema://$host {
|
||||
return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true);
|
||||
}
|
||||
|
||||
public function skipServer()
|
||||
{
|
||||
if ($this->ip === '1.2.3.4') {
|
||||
// ray('skipping 1.2.3.4');
|
||||
return true;
|
||||
}
|
||||
if ($this->settings->force_disabled === true) {
|
||||
// ray('force_disabled');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isForceDisabled()
|
||||
{
|
||||
return $this->settings->force_disabled;
|
||||
@@ -447,124 +514,112 @@ $schema://$host {
|
||||
|
||||
public function forceEnableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => false,
|
||||
]);
|
||||
$this->settings->force_disabled = false;
|
||||
$this->settings->save();
|
||||
}
|
||||
|
||||
public function forceDisableServer()
|
||||
{
|
||||
$this->settings->update([
|
||||
'force_disabled' => true,
|
||||
]);
|
||||
$this->settings->force_disabled = true;
|
||||
$this->settings->save();
|
||||
$sshKeyFileLocation = "id.root@{$this->uuid}";
|
||||
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
|
||||
public function sentinelHeartbeat(bool $isReset = false)
|
||||
{
|
||||
$this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the wait time for Sentinel to push before performing an SSH check.
|
||||
*
|
||||
* @return int The wait time in seconds.
|
||||
*/
|
||||
public function waitBeforeDoingSshCheck(): int
|
||||
{
|
||||
$wait = $this->settings->sentinel_push_interval_seconds * 3;
|
||||
if ($wait < 120) {
|
||||
$wait = 120;
|
||||
}
|
||||
|
||||
return $wait;
|
||||
}
|
||||
|
||||
public function isSentinelLive()
|
||||
{
|
||||
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck()));
|
||||
}
|
||||
|
||||
public function isSentinelEnabled()
|
||||
{
|
||||
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
|
||||
}
|
||||
|
||||
public function isMetricsEnabled()
|
||||
{
|
||||
return $this->settings->is_metrics_enabled;
|
||||
}
|
||||
|
||||
public function isServerApiEnabled()
|
||||
{
|
||||
return $this->settings->is_sentinel_enabled;
|
||||
}
|
||||
|
||||
public function checkSentinel()
|
||||
{
|
||||
ray("Checking sentinel on server: {$this->name}");
|
||||
if ($this->is_metrics_enabled) {
|
||||
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
|
||||
$sentinel_found = json_decode($sentinel_found, true);
|
||||
$status = data_get($sentinel_found, '0.State.Status', 'exited');
|
||||
if ($status !== 'running') {
|
||||
ray('Sentinel is not running, starting it...');
|
||||
PullSentinelImageJob::dispatch($this);
|
||||
} else {
|
||||
ray('Sentinel is running');
|
||||
CheckAndStartSentinelJob::dispatch($this);
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
if (str($cpu)->contains('error')) {
|
||||
$error = json_decode($cpu, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$cpu = json_decode($cpu, true);
|
||||
|
||||
return collect($cpu)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getMetrics()
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
if ($this->is_metrics_enabled) {
|
||||
$from = now()->subMinutes(5)->toIso8601ZuluString();
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
$cpu = str($cpu)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($cpu)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $value] = explode(',', trim($line));
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
||||
if (str($memory)->contains('error')) {
|
||||
$error = json_decode($memory, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$memory = json_decode($memory, true);
|
||||
$parsedCollection = collect($memory)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['usedPercent']];
|
||||
});
|
||||
|
||||
return [(int) $time, (float) $value];
|
||||
});
|
||||
})->toArray();
|
||||
|
||||
return $parsedCollection;
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function isServerReady(int $tries = 3)
|
||||
public function getDiskUsage(): ?string
|
||||
{
|
||||
if ($this->skipServer()) {
|
||||
return false;
|
||||
}
|
||||
$serverUptimeCheckNumber = $this->unreachable_count;
|
||||
if ($this->unreachable_count < $tries) {
|
||||
$serverUptimeCheckNumber = $this->unreachable_count + 1;
|
||||
}
|
||||
if ($this->unreachable_count > $tries) {
|
||||
$serverUptimeCheckNumber = $tries;
|
||||
}
|
||||
|
||||
$serverUptimeCheckNumberMax = $tries;
|
||||
|
||||
// ray('server: ' . $this->name);
|
||||
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
|
||||
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
|
||||
|
||||
['uptime' => $uptime] = $this->validateConnection();
|
||||
if ($uptime) {
|
||||
if ($this->unreachable_notification_sent === true) {
|
||||
$this->update(['unreachable_notification_sent' => false]);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
|
||||
// Reached max number of retries
|
||||
if ($this->unreachable_notification_sent === false) {
|
||||
ray('Server unreachable, sending notification...');
|
||||
// $this->team?->notify(new Unreachable($this));
|
||||
$this->update(['unreachable_notification_sent' => true]);
|
||||
}
|
||||
if ($this->settings->is_reachable === true) {
|
||||
$this->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($this->applications() as $application) {
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->databases() as $database) {
|
||||
$database->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->services()->get() 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']);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->update([
|
||||
'unreachable_count' => $this->unreachable_count + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function getDiskUsage()
|
||||
{
|
||||
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
|
||||
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
|
||||
// return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
|
||||
}
|
||||
|
||||
public function definedResources()
|
||||
@@ -591,7 +646,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 -aq) --format '{{json .}}'"], $this, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
$containerReplicates = collect([]);
|
||||
}
|
||||
|
||||
return [
|
||||
'containers' => collect($containers) ?? collect([]),
|
||||
'containerReplicates' => collect($containerReplicates) ?? collect([]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getContainersWithSentinel(): Collection
|
||||
{
|
||||
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
|
||||
$sentinel_found = json_decode($sentinel_found, true);
|
||||
@@ -604,24 +701,21 @@ $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);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadAllContainers(): Collection
|
||||
{
|
||||
if ($this->isFunctional()) {
|
||||
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
|
||||
return collect($containers);
|
||||
}
|
||||
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
public function loadUnmanagedContainers(): Collection
|
||||
{
|
||||
if ($this->isFunctional()) {
|
||||
@@ -668,9 +762,9 @@ $schema://$host {
|
||||
$clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
|
||||
|
||||
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
|
||||
})->filter(function ($item) {
|
||||
})->flatten()->filter(function ($item) {
|
||||
return data_get($item, 'name') !== 'coolify-db';
|
||||
})->flatten();
|
||||
});
|
||||
}
|
||||
|
||||
public function applications()
|
||||
@@ -714,6 +808,33 @@ $schema://$host {
|
||||
return $this->hasMany(Service::class);
|
||||
}
|
||||
|
||||
public function port(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
return preg_replace('/[^0-9]/', '', $value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
return preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function ip(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
return preg_replace('/[^0-9a-zA-Z.:%-]/', '', $value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getIp(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -744,8 +865,6 @@ $schema://$host {
|
||||
$standalone_docker = $this->hasMany(StandaloneDocker::class)->get();
|
||||
$swarm_docker = $this->hasMany(SwarmDocker::class)->get();
|
||||
|
||||
// $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get();
|
||||
// return $standalone_docker->concat($swarm_docker)->concat($additional_dockers);
|
||||
return $standalone_docker->concat($swarm_docker);
|
||||
}
|
||||
|
||||
@@ -766,7 +885,7 @@ $schema://$host {
|
||||
|
||||
public function muxFilename()
|
||||
{
|
||||
return "{$this->ip}_{$this->port}_{$this->user}";
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
public function team()
|
||||
@@ -776,20 +895,32 @@ $schema://$host {
|
||||
|
||||
public function isProxyShouldRun()
|
||||
{
|
||||
if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
|
||||
// TODO: Do we need "|| $this->proxy->force_stop" here?
|
||||
if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function skipServer()
|
||||
{
|
||||
if ($this->ip === '1.2.3.4') {
|
||||
return true;
|
||||
}
|
||||
if ($this->settings->force_disabled === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isFunctional()
|
||||
{
|
||||
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
|
||||
['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
|
||||
if (! $isFunctional) {
|
||||
Storage::disk('ssh-keys')->delete($private_key_filename);
|
||||
Storage::disk('ssh-mux')->delete($mux_filename);
|
||||
$isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4';
|
||||
|
||||
if ($isFunctional === false) {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
|
||||
return $isFunctional;
|
||||
@@ -806,7 +937,7 @@ $schema://$host {
|
||||
$releaseLines = collect(explode("\n", $os_release));
|
||||
$collectedData = collect([]);
|
||||
foreach ($releaseLines as $line) {
|
||||
$item = Str::of($line)->trim();
|
||||
$item = str($line)->trim();
|
||||
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
|
||||
}
|
||||
$ID = data_get($collectedData, 'ID');
|
||||
@@ -841,36 +972,110 @@ $schema://$host {
|
||||
return data_get($this, 'settings.is_swarm_worker');
|
||||
}
|
||||
|
||||
public function validateConnection()
|
||||
public function serverStatus(): bool
|
||||
{
|
||||
config()->set('coolify.mux_enabled', false);
|
||||
|
||||
$server = Server::find($this->id);
|
||||
if (! $server) {
|
||||
return ['uptime' => false, 'error' => 'Server not found.'];
|
||||
if ($this->status() === false) {
|
||||
return false;
|
||||
}
|
||||
if ($server->skipServer()) {
|
||||
if ($this->isFunctional() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function status(): bool
|
||||
{
|
||||
['uptime' => $uptime] = $this->validateConnection(false);
|
||||
if ($uptime === false) {
|
||||
foreach ($this->applications() as $application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
}
|
||||
foreach ($this->databases() as $database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
}
|
||||
foreach ($this->services() as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
$app->status = 'exited';
|
||||
$app->save();
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
$db->status = 'exited';
|
||||
$db->save();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isReachableChanged()
|
||||
{
|
||||
$this->refresh();
|
||||
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
|
||||
$isReachable = (bool) $this->settings->is_reachable;
|
||||
// If the server is reachable, send the reachable notification if it was sent before
|
||||
if ($isReachable === true) {
|
||||
if ($unreachableNotificationSent === true) {
|
||||
$this->sendReachableNotification();
|
||||
}
|
||||
} else {
|
||||
// If the server is unreachable, send the unreachable notification if it was not sent before
|
||||
if ($unreachableNotificationSent === false) {
|
||||
$this->sendUnreachableNotification();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function sendReachableNotification()
|
||||
{
|
||||
$this->unreachable_notification_sent = false;
|
||||
$this->save();
|
||||
$this->refresh();
|
||||
$this->team->notify(new Reachable($this));
|
||||
}
|
||||
|
||||
public function sendUnreachableNotification()
|
||||
{
|
||||
$this->unreachable_notification_sent = true;
|
||||
$this->save();
|
||||
$this->refresh();
|
||||
$this->team->notify(new Unreachable($this));
|
||||
}
|
||||
|
||||
public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
|
||||
{
|
||||
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
|
||||
|
||||
if ($this->skipServer()) {
|
||||
return ['uptime' => false, 'error' => 'Server skipped.'];
|
||||
}
|
||||
try {
|
||||
// EC2 does not have `uptime` command, lol
|
||||
instant_remote_process(['ls /'], $server);
|
||||
$server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$server->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
if (data_get($server, 'unreachable_notification_sent') === true) {
|
||||
// $server->team?->notify(new Revived($server));
|
||||
$server->update(['unreachable_notification_sent' => false]);
|
||||
// Make sure the private key is stored
|
||||
if ($this->privateKey) {
|
||||
$this->privateKey->storeInFileSystem();
|
||||
}
|
||||
instant_remote_process(['ls /'], $this);
|
||||
if ($this->settings->is_reachable === false) {
|
||||
$this->settings->is_reachable = true;
|
||||
$this->settings->save();
|
||||
}
|
||||
|
||||
return ['uptime' => true, 'error' => null];
|
||||
} catch (\Throwable $e) {
|
||||
$server->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
if ($justCheckingNewKey) {
|
||||
return ['uptime' => false, 'error' => 'This key is not valid for this server.'];
|
||||
}
|
||||
if ($this->settings->is_reachable === true) {
|
||||
$this->settings->is_reachable = false;
|
||||
$this->settings->save();
|
||||
}
|
||||
|
||||
return ['uptime' => false, 'error' => $e->getMessage()];
|
||||
}
|
||||
@@ -878,9 +1083,7 @@ $schema://$host {
|
||||
|
||||
public function installDocker()
|
||||
{
|
||||
$activity = InstallDocker::run($this);
|
||||
|
||||
return $activity;
|
||||
return InstallDocker::run($this);
|
||||
}
|
||||
|
||||
public function validateDockerEngine($throwError = false)
|
||||
@@ -991,4 +1194,61 @@ $schema://$host {
|
||||
{
|
||||
return $this->settings->is_build_server;
|
||||
}
|
||||
|
||||
public static function createWithPrivateKey(array $data, PrivateKey $privateKey)
|
||||
{
|
||||
$server = new self($data);
|
||||
$server->privateKey()->associate($privateKey);
|
||||
$server->save();
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null)
|
||||
{
|
||||
$this->update($data);
|
||||
if ($privateKey) {
|
||||
$this->privateKey()->associate($privateKey);
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function storageCheck(): ?string
|
||||
{
|
||||
$commands = [
|
||||
'df / --output=pcent | tr -cd 0-9',
|
||||
];
|
||||
|
||||
return instant_remote_process($commands, $this, false);
|
||||
}
|
||||
|
||||
public function isIpv6(): bool
|
||||
{
|
||||
return str($this->ip)->contains(':');
|
||||
}
|
||||
|
||||
public function restartSentinel(bool $async = true)
|
||||
{
|
||||
try {
|
||||
if ($async) {
|
||||
StartSentinel::dispatch($this, true);
|
||||
} else {
|
||||
StartSentinel::run($this, true);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
return base_url().'/server/'.$this->uuid;
|
||||
}
|
||||
|
||||
public function restartContainer(string $containerName)
|
||||
{
|
||||
return instant_remote_process(['docker restart '.$containerName], $this, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,151 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Server Settings model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer'],
|
||||
'concurrent_builds' => ['type' => 'integer'],
|
||||
'dynamic_timeout' => ['type' => 'integer'],
|
||||
'force_disabled' => ['type' => 'boolean'],
|
||||
'force_server_cleanup' => ['type' => 'boolean'],
|
||||
'is_build_server' => ['type' => 'boolean'],
|
||||
'is_cloudflare_tunnel' => ['type' => 'boolean'],
|
||||
'is_jump_server' => ['type' => 'boolean'],
|
||||
'is_logdrain_axiom_enabled' => ['type' => 'boolean'],
|
||||
'is_logdrain_custom_enabled' => ['type' => 'boolean'],
|
||||
'is_logdrain_highlight_enabled' => ['type' => 'boolean'],
|
||||
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
|
||||
'is_metrics_enabled' => ['type' => 'boolean'],
|
||||
'is_reachable' => ['type' => 'boolean'],
|
||||
'is_sentinel_enabled' => ['type' => 'boolean'],
|
||||
'is_swarm_manager' => ['type' => 'boolean'],
|
||||
'is_swarm_worker' => ['type' => 'boolean'],
|
||||
'is_usable' => ['type' => 'boolean'],
|
||||
'logdrain_axiom_api_key' => ['type' => 'string'],
|
||||
'logdrain_axiom_dataset_name' => ['type' => 'string'],
|
||||
'logdrain_custom_config' => ['type' => 'string'],
|
||||
'logdrain_custom_config_parser' => ['type' => 'string'],
|
||||
'logdrain_highlight_project_id' => ['type' => 'string'],
|
||||
'logdrain_newrelic_base_uri' => ['type' => 'string'],
|
||||
'logdrain_newrelic_license_key' => ['type' => 'string'],
|
||||
'sentinel_metrics_history_days' => ['type' => 'integer'],
|
||||
'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
|
||||
'sentinel_token' => ['type' => 'string'],
|
||||
'docker_cleanup_frequency' => ['type' => 'string'],
|
||||
'docker_cleanup_threshold' => ['type' => 'integer'],
|
||||
'server_id' => ['type' => 'integer'],
|
||||
'wildcard_domain' => ['type' => 'string'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
'updated_at' => ['type' => 'string'],
|
||||
]
|
||||
)]
|
||||
class ServerSetting extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'force_docker_cleanup' => 'boolean',
|
||||
'docker_cleanup_threshold' => 'integer',
|
||||
'sentinel_token' => 'encrypted',
|
||||
'is_reachable' => 'boolean',
|
||||
'is_usable' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
try {
|
||||
if (str($setting->sentinel_token)->isEmpty()) {
|
||||
$setting->generateSentinelToken(save: false, ignoreEvent: true);
|
||||
}
|
||||
if (str($setting->sentinel_custom_url)->isEmpty()) {
|
||||
$setting->generateSentinelUrl(save: false, ignoreEvent: true);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Error creating server setting: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
static::updated(function ($settings) {
|
||||
if (
|
||||
$settings->isDirty('sentinel_token') ||
|
||||
$settings->isDirty('sentinel_custom_url') ||
|
||||
$settings->isDirty('sentinel_metrics_refresh_rate_seconds') ||
|
||||
$settings->isDirty('sentinel_metrics_history_days') ||
|
||||
$settings->isDirty('sentinel_push_interval_seconds')
|
||||
) {
|
||||
$settings->server->restartSentinel();
|
||||
}
|
||||
if ($settings->isDirty('is_reachable')) {
|
||||
$settings->server->isReachableChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->server->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->sentinel_token = $encrypted;
|
||||
if ($save) {
|
||||
if ($ignoreEvent) {
|
||||
$this->saveQuietly();
|
||||
} else {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
|
||||
{
|
||||
$domain = null;
|
||||
$settings = InstanceSettings::get();
|
||||
if ($this->server->isLocalhost()) {
|
||||
$domain = 'http://host.docker.internal:8000';
|
||||
} elseif ($settings->fqdn) {
|
||||
$domain = $settings->fqdn;
|
||||
} elseif ($settings->public_ipv4) {
|
||||
$domain = 'http://'.$settings->public_ipv4.':8000';
|
||||
} elseif ($settings->public_ipv6) {
|
||||
$domain = 'http://'.$settings->public_ipv6.':8000';
|
||||
}
|
||||
$this->sentinel_custom_url = $domain;
|
||||
if ($save) {
|
||||
if ($ignoreEvent) {
|
||||
$this->saveQuietly();
|
||||
} else {
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function dockerCleanupFrequency(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
},
|
||||
get: function ($value) {
|
||||
return translate_cron_expression($value);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,60 @@
|
||||
|
||||
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\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Service model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the service. Only used for database identification.'],
|
||||
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the service.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the service.'],
|
||||
'environment_id' => ['type' => 'integer', 'description' => 'The unique identifier of the environment where the service is attached to.'],
|
||||
'server_id' => ['type' => 'integer', 'description' => 'The unique identifier of the server where the service is running.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The description of the service.'],
|
||||
'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'],
|
||||
'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'],
|
||||
'destination_type' => ['type' => 'string', 'description' => 'Destination type.'],
|
||||
'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'],
|
||||
'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'],
|
||||
'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'],
|
||||
'is_container_label_readonly_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label readonly.'],
|
||||
'config_hash' => ['type' => 'string', 'description' => 'The hash of the service configuration.'],
|
||||
'service_type' => ['type' => 'string', 'description' => 'The type of the service.'],
|
||||
'created_at' => ['type' => 'string', 'description' => 'The date and time when the service was created.'],
|
||||
'updated_at' => ['type' => 'string', 'description' => 'The date and time when the service was last updated.'],
|
||||
'deleted_at' => ['type' => 'string', 'description' => 'The date and time when the service was deleted.'],
|
||||
],
|
||||
)]
|
||||
class Service extends BaseModel
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '4';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['server_status'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($service) {
|
||||
$service->compose_parsing_version = self::$parserVersion;
|
||||
$service->save();
|
||||
});
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$domains = $this->applications()->get()->pluck('fqdn')->sort()->toArray();
|
||||
@@ -51,6 +94,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');
|
||||
@@ -76,15 +133,86 @@ class Service extends BaseModel
|
||||
return $this->morphToMany(Tag::class, 'taggable');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public function getContainersToStop(): array
|
||||
{
|
||||
$containersToStop = [];
|
||||
$applications = $this->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$containersToStop[] = "{$application->name}-{$this->uuid}";
|
||||
}
|
||||
$dbs = $this->databases()->get();
|
||||
foreach ($dbs as $db) {
|
||||
$containersToStop[] = "{$db->name}-{$this->uuid}";
|
||||
}
|
||||
|
||||
return $containersToStop;
|
||||
}
|
||||
|
||||
public function stopContainers(array $containerNames, $server, int $timeout = 300)
|
||||
{
|
||||
$processes = [];
|
||||
foreach ($containerNames as $containerName) {
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||
}
|
||||
|
||||
$startTime = time();
|
||||
while (count($processes) > 0) {
|
||||
$finishedProcesses = array_filter($processes, function ($process) {
|
||||
return ! $process->running();
|
||||
});
|
||||
foreach (array_keys($finishedProcesses) as $containerName) {
|
||||
unset($processes[$containerName]);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
|
||||
if (time() - $startTime >= $timeout) {
|
||||
$this->forceStopRemainingContainers(array_keys($processes), $server);
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
public function stopContainer(string $containerName, int $timeout): InvokedProcess
|
||||
{
|
||||
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
}
|
||||
|
||||
public function removeContainer(string $containerName, $server)
|
||||
{
|
||||
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
|
||||
}
|
||||
|
||||
public function forceStopRemainingContainers(array $containerNames, $server)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
{
|
||||
$server = data_get($this, 'server');
|
||||
$server = data_get($this, 'destination.server');
|
||||
$workdir = $this->workdir();
|
||||
if (str($workdir)->endsWith($this->uuid)) {
|
||||
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_connected_networks($uuid)
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
$applications = $this->applications;
|
||||
@@ -160,9 +288,196 @@ class Service extends BaseModel
|
||||
$fields = collect([]);
|
||||
$applications = $this->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
$image = str($application->image)->before(':');
|
||||
if ($image->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
switch ($image) {
|
||||
case str($image)?->contains('tolgee'):
|
||||
case $image->contains('castopod'):
|
||||
$data = collect([]);
|
||||
$disable_https = $this->environment_variables()->where('key', 'CP_DISABLE_HTTPS')->first();
|
||||
if ($disable_https) {
|
||||
$data = $data->merge([
|
||||
'Disable HTTPS' => [
|
||||
'key' => 'CP_DISABLE_HTTPS',
|
||||
'value' => data_get($disable_https, 'value'),
|
||||
'rules' => 'required',
|
||||
'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Castopod', $data->toArray());
|
||||
break;
|
||||
case $image->contains('label-studio'):
|
||||
$data = collect([]);
|
||||
$username = $this->environment_variables()->where('key', 'LABEL_STUDIO_USERNAME')->first();
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LABELSTUDIO')->first();
|
||||
if ($username) {
|
||||
$data = $data->merge([
|
||||
'Username' => [
|
||||
'key' => 'LABEL_STUDIO_USERNAME',
|
||||
'value' => data_get($username, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Label Studio', $data->toArray());
|
||||
break;
|
||||
case $image->contains('litellm'):
|
||||
$data = collect([]);
|
||||
$username = $this->environment_variables()->where('key', 'SERVICE_USER_UI')->first();
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UI')->first();
|
||||
if ($username) {
|
||||
$data = $data->merge([
|
||||
'Username' => [
|
||||
'key' => data_get($username, 'key'),
|
||||
'value' => data_get($username, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Litellm', $data->toArray());
|
||||
break;
|
||||
case $image->contains('langfuse'):
|
||||
$data = collect([]);
|
||||
$email = $this->environment_variables()->where('key', 'LANGFUSE_INIT_USER_EMAIL')->first();
|
||||
if ($email) {
|
||||
$data = $data->merge([
|
||||
'Admin Email' => [
|
||||
'key' => data_get($email, 'key'),
|
||||
'value' => data_get($email, 'value'),
|
||||
'rules' => 'required|email',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first();
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Admin Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Langfuse', $data->toArray());
|
||||
break;
|
||||
case $image->contains('invoiceninja'):
|
||||
$data = collect([]);
|
||||
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
|
||||
$data = $data->merge([
|
||||
'Email' => [
|
||||
'key' => data_get($email, 'key'),
|
||||
'value' => data_get($email, 'value'),
|
||||
'rules' => 'required|email',
|
||||
],
|
||||
]);
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
$fields->put('Invoice Ninja', $data->toArray());
|
||||
break;
|
||||
case $image->contains('argilla'):
|
||||
$data = collect([]);
|
||||
$api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first();
|
||||
$data = $data->merge([
|
||||
'API Key' => [
|
||||
'key' => data_get($api_key, 'key'),
|
||||
'value' => data_get($api_key, 'value'),
|
||||
'isPassword' => true,
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
$data = $data->merge([
|
||||
'API Key' => [
|
||||
'key' => data_get($api_key, 'key'),
|
||||
'value' => data_get($api_key, 'value'),
|
||||
'isPassword' => true,
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
$username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first();
|
||||
$data = $data->merge([
|
||||
'Username' => [
|
||||
'key' => data_get($username, 'key'),
|
||||
'value' => data_get($username, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first();
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
$fields->put('Argilla', $data->toArray());
|
||||
break;
|
||||
case $image->contains('rabbitmq'):
|
||||
$data = collect([]);
|
||||
$host_port = $this->environment_variables()->where('key', 'PORT')->first();
|
||||
$username = $this->environment_variables()->where('key', 'SERVICE_USER_RABBITMQ')->first();
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_RABBITMQ')->first();
|
||||
if ($host_port) {
|
||||
$data = $data->merge([
|
||||
'Host Port Binding' => [
|
||||
'key' => data_get($host_port, 'key'),
|
||||
'value' => data_get($host_port, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($username) {
|
||||
$data = $data->merge([
|
||||
'Username' => [
|
||||
'key' => data_get($username, 'key'),
|
||||
'value' => data_get($username, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('RabbitMQ', $data->toArray());
|
||||
break;
|
||||
case $image->contains('tolgee'):
|
||||
$data = collect([]);
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
|
||||
$data = $data->merge([
|
||||
@@ -176,7 +491,7 @@ class Service extends BaseModel
|
||||
if ($admin_password) {
|
||||
$data = $data->merge([
|
||||
'Admin Password' => [
|
||||
'key' => 'SERVICE_PASSWORD_TOLGEE',
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
@@ -185,7 +500,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Tolgee', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('logto'):
|
||||
case $image->contains('logto'):
|
||||
$data = collect([]);
|
||||
$logto_endpoint = $this->environment_variables()->where('key', 'LOGTO_ENDPOINT')->first();
|
||||
$logto_admin_endpoint = $this->environment_variables()->where('key', 'LOGTO_ADMIN_ENDPOINT')->first();
|
||||
@@ -209,7 +524,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Logto', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('unleash-server'):
|
||||
case $image->contains('unleash-server'):
|
||||
$data = collect([]);
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_UNLEASH')->first();
|
||||
$data = $data->merge([
|
||||
@@ -223,7 +538,7 @@ class Service extends BaseModel
|
||||
if ($admin_password) {
|
||||
$data = $data->merge([
|
||||
'Admin Password' => [
|
||||
'key' => 'SERVICE_PASSWORD_UNLEASH',
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
@@ -232,7 +547,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Unleash', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('grafana'):
|
||||
case $image->contains('grafana'):
|
||||
$data = collect([]);
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
|
||||
$data = $data->merge([
|
||||
@@ -246,7 +561,7 @@ class Service extends BaseModel
|
||||
if ($admin_password) {
|
||||
$data = $data->merge([
|
||||
'Admin Password' => [
|
||||
'key' => 'GF_SECURITY_ADMIN_PASSWORD',
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
@@ -255,7 +570,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Grafana', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('directus'):
|
||||
case $image->contains('directus'):
|
||||
$data = collect([]);
|
||||
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
|
||||
@@ -281,7 +596,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Directus', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('kong'):
|
||||
case $image->contains('kong'):
|
||||
$data = collect([]);
|
||||
$dashboard_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||
$dashboard_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
|
||||
@@ -305,7 +620,7 @@ class Service extends BaseModel
|
||||
]);
|
||||
}
|
||||
$fields->put('Supabase', $data->toArray());
|
||||
case str($image)?->contains('minio'):
|
||||
case $image->contains('minio'):
|
||||
$data = collect([]);
|
||||
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
|
||||
@@ -358,7 +673,7 @@ class Service extends BaseModel
|
||||
|
||||
$fields->put('MinIO', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('weblate'):
|
||||
case $image->contains('weblate'):
|
||||
$data = collect([]);
|
||||
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
|
||||
@@ -384,7 +699,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Weblate', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('meilisearch'):
|
||||
case $image->contains('meilisearch'):
|
||||
$data = collect([]);
|
||||
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
|
||||
if ($SERVICE_PASSWORD_MEILISEARCH) {
|
||||
@@ -398,7 +713,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Meilisearch', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('ghost'):
|
||||
case $image->contains('ghost'):
|
||||
$data = collect([]);
|
||||
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
|
||||
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
|
||||
@@ -458,33 +773,8 @@ class Service extends BaseModel
|
||||
|
||||
$fields->put('Ghost', $data->toArray());
|
||||
break;
|
||||
default:
|
||||
$data = collect([]);
|
||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
|
||||
if ($admin_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => 'SERVICE_USER_ADMIN',
|
||||
'value' => data_get($admin_user, 'value', 'admin'),
|
||||
'readonly' => true,
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => 'SERVICE_PASSWORD_ADMIN',
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Admin', $data->toArray());
|
||||
break;
|
||||
case str($image)?->contains('vaultwarden'):
|
||||
|
||||
case $image->contains('vaultwarden'):
|
||||
$data = collect([]);
|
||||
|
||||
$DATABASE_URL = $this->environment_variables()->where('key', 'DATABASE_URL')->first();
|
||||
@@ -550,14 +840,128 @@ class Service extends BaseModel
|
||||
|
||||
$fields->put('Vaultwarden', $data);
|
||||
break;
|
||||
case $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' => 'GITLAB_ROOT_USER',
|
||||
'value' => 'root',
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$fields->put('GitLab', $data->toArray());
|
||||
break;
|
||||
case $image->contains('code-server'):
|
||||
$data = collect([]);
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_PASSWORDCODESERVER')->first();
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$sudoPassword = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_SUDOCODESERVER')->first();
|
||||
if ($sudoPassword) {
|
||||
$data = $data->merge([
|
||||
'Sudo Password' => [
|
||||
'key' => data_get($sudoPassword, 'key'),
|
||||
'value' => data_get($sudoPassword, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Code Server', $data->toArray());
|
||||
break;
|
||||
case $image->contains('elestio/strapi'):
|
||||
$data = collect([]);
|
||||
$license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
|
||||
if ($license) {
|
||||
$data = $data->merge([
|
||||
'License' => [
|
||||
'key' => data_get($license, 'key'),
|
||||
'value' => data_get($license, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
$nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
|
||||
if ($nodeEnv) {
|
||||
$data = $data->merge([
|
||||
'Node Environment' => [
|
||||
'key' => data_get($nodeEnv, 'key'),
|
||||
'value' => data_get($nodeEnv, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$fields->put('Strapi', $data->toArray());
|
||||
break;
|
||||
default:
|
||||
$data = collect([]);
|
||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||
// Chaskiq
|
||||
$admin_email = $this->environment_variables()->where('key', 'ADMIN_EMAIL')->first();
|
||||
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
|
||||
if ($admin_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($admin_user, 'key'),
|
||||
'value' => data_get($admin_user, 'value', 'admin'),
|
||||
'readonly' => true,
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($admin_email) {
|
||||
$data = $data->merge([
|
||||
'Email' => [
|
||||
'key' => data_get($admin_email, 'key'),
|
||||
'value' => data_get($admin_email, 'value'),
|
||||
'rules' => 'required|email',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Admin', $data->toArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
$databases = $this->databases()->get();
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$image = str($database->image)->before(':')->value();
|
||||
$image = str($database->image)->before(':');
|
||||
if ($image->isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
switch ($image) {
|
||||
case str($image)->contains('postgres'):
|
||||
case $image->contains('postgres'):
|
||||
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
|
||||
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
|
||||
@@ -595,10 +999,10 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('PostgreSQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
|
||||
case $image->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
@@ -645,11 +1049,11 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('MySQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
|
||||
case $image->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
|
||||
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
@@ -739,12 +1143,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;
|
||||
@@ -818,12 +1234,12 @@ class Service extends BaseModel
|
||||
|
||||
public function environment_variables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->orderBy('key', 'asc');
|
||||
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
|
||||
}
|
||||
|
||||
public function environment_variables_preview(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
@@ -834,17 +1250,50 @@ class Service extends BaseModel
|
||||
public function saveComposeConfigs()
|
||||
{
|
||||
$workdir = $this->workdir();
|
||||
$commands[] = "mkdir -p $workdir";
|
||||
$commands[] = "cd $workdir";
|
||||
|
||||
$docker_compose_base64 = base64_encode($this->docker_compose);
|
||||
$commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
|
||||
$envs = $this->environment_variables()->get();
|
||||
instant_remote_process([
|
||||
"mkdir -p $workdir",
|
||||
"cd $workdir",
|
||||
], $this->server);
|
||||
|
||||
$filename = new Cuid2.'-docker-compose.yml';
|
||||
Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose);
|
||||
$path = Storage::path("tmp/{$filename}");
|
||||
instant_scp($path, "{$workdir}/docker-compose.yml", $this->server);
|
||||
Storage::disk('local')->delete("tmp/{$filename}");
|
||||
|
||||
$commands[] = "cd $workdir";
|
||||
$commands[] = 'rm -f .env || true';
|
||||
foreach ($envs as $env) {
|
||||
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
|
||||
|
||||
$envs_from_coolify = $this->environment_variables()->get();
|
||||
$sorted = $envs_from_coolify->sortBy(function ($env) {
|
||||
if (str($env->key)->startsWith('SERVICE_')) {
|
||||
return 1;
|
||||
}
|
||||
if (str($env->value)->startsWith('$SERVICE_') || str($env->value)->startsWith('${SERVICE_')) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 3;
|
||||
});
|
||||
foreach ($sorted as $env) {
|
||||
if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
|
||||
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
|
||||
} else {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
|
||||
}
|
||||
}
|
||||
if ($envs->count() === 0) {
|
||||
if ($sorted->count() === 0) {
|
||||
$commands[] = 'touch .env';
|
||||
}
|
||||
instant_remote_process($commands, $this->server);
|
||||
@@ -852,14 +1301,33 @@ class Service extends BaseModel
|
||||
|
||||
public function parse(bool $isNew = false): Collection
|
||||
{
|
||||
return parseDockerComposeFile($this, $isNew);
|
||||
if ((int) $this->compose_parsing_version >= 3) {
|
||||
return newParser($this);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile($this, $isNew);
|
||||
} else {
|
||||
return collect([]);
|
||||
}
|
||||
}
|
||||
|
||||
public function networks()
|
||||
{
|
||||
$networks = getTopLevelNetworks($this);
|
||||
return getTopLevelNetworks($this);
|
||||
}
|
||||
|
||||
// ray($networks);
|
||||
return $networks;
|
||||
protected function isDeployable(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
$envs = $this->environment_variables()->where('is_required', true)->get();
|
||||
foreach ($envs as $env) {
|
||||
if ($env->is_really_required) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ class ServiceApplication extends BaseModel
|
||||
$service->persistentStorages()->delete();
|
||||
$service->fileStorages()->delete();
|
||||
});
|
||||
static::saving(function ($service) {
|
||||
if ($service->isDirty('status')) {
|
||||
$service->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function restart()
|
||||
@@ -27,6 +32,26 @@ class ServiceApplication extends BaseModel
|
||||
instant_remote_process(["docker restart {$container_id}"], $this->service->server);
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return str($this->status)->contains('exited');
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
@@ -97,4 +122,9 @@ class ServiceApplication extends BaseModel
|
||||
{
|
||||
getFilesystemVolumesFromServer($this, $isInit);
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,21 @@ class ServiceDatabase extends BaseModel
|
||||
$service->persistentStorages()->delete();
|
||||
$service->fileStorages()->delete();
|
||||
});
|
||||
static::saving(function ($service) {
|
||||
if ($service->isDirty('status')) {
|
||||
$service->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public function restart()
|
||||
@@ -25,6 +40,16 @@ class ServiceDatabase extends BaseModel
|
||||
remote_process(["docker restart {$container_id}"], $this->service->server);
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return str($this->status)->contains('exited');
|
||||
}
|
||||
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
@@ -105,4 +130,13 @@ class ServiceDatabase extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return str($this->databaseType())->contains('mysql') ||
|
||||
str($this->databaseType())->contains('postgres') ||
|
||||
str($this->databaseType())->contains('postgis') ||
|
||||
str($this->databaseType())->contains('mariadb') ||
|
||||
str($this->databaseType())->contains('mongodb');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneClickhouse extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'clickhouse_password' => 'encrypted',
|
||||
];
|
||||
@@ -29,19 +32,26 @@ class StandaloneClickhouse extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$database->scheduledBackups()->delete();
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -70,6 +80,11 @@ class StandaloneClickhouse extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -89,6 +104,17 @@ class StandaloneClickhouse extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -178,18 +204,36 @@ class StandaloneClickhouse extends BaseModel
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-clickhouse';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
} else {
|
||||
return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -226,4 +270,53 @@ class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneDragonfly extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'dragonfly_password' => 'encrypted',
|
||||
];
|
||||
@@ -29,19 +32,26 @@ class StandaloneDragonfly extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$database->scheduledBackups()->delete();
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -70,6 +80,11 @@ class StandaloneDragonfly extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -89,6 +104,17 @@ class StandaloneDragonfly extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -178,18 +204,36 @@ class StandaloneDragonfly extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-dragonfly';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
} else {
|
||||
return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -226,4 +270,53 @@ class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneKeydb extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'keydb_password' => 'encrypted',
|
||||
];
|
||||
@@ -29,19 +32,26 @@ class StandaloneKeydb extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$database->scheduledBackups()->delete();
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -70,6 +80,11 @@ class StandaloneKeydb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -89,6 +104,17 @@ class StandaloneKeydb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -178,18 +204,36 @@ class StandaloneKeydb extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-keydb';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
} else {
|
||||
return "redis://{$this->keydb_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "redis://:{$this->keydb_password}@{$this->uuid}:6379/0",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "redis://:{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -226,4 +270,53 @@ class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneMariadb extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'mariadb_password' => 'encrypted',
|
||||
];
|
||||
@@ -29,19 +32,26 @@ class StandaloneMariadb extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$database->scheduledBackups()->delete();
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -70,6 +80,11 @@ class StandaloneMariadb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -89,6 +104,17 @@ class StandaloneMariadb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -161,6 +187,13 @@ class StandaloneMariadb extends BaseModel
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-mariadb';
|
||||
@@ -183,13 +216,24 @@ class StandaloneMariadb extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
} else {
|
||||
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -226,4 +270,53 @@ class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneMongodb extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
@@ -33,19 +36,26 @@ class StandaloneMongodb extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$database->scheduledBackups()->delete();
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -74,6 +84,11 @@ class StandaloneMongodb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -93,6 +108,17 @@ class StandaloneMongodb extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -198,18 +224,36 @@ class StandaloneMongodb extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-mongodb';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false)
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
||||
} else {
|
||||
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -246,4 +290,53 @@ class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneMysql extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'mysql_password' => 'encrypted',
|
||||
'mysql_root_password' => 'encrypted',
|
||||
@@ -30,19 +33,26 @@ class StandaloneMysql extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$database->scheduledBackups()->delete();
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -71,6 +81,11 @@ class StandaloneMysql extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -90,6 +105,17 @@ class StandaloneMysql extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -157,6 +183,13 @@ class StandaloneMysql extends BaseModel
|
||||
return null;
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-mysql';
|
||||
@@ -184,13 +217,24 @@ class StandaloneMysql extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
} else {
|
||||
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -227,4 +271,53 @@ class StandaloneMysql extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandalonePostgresql extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'init_scripts' => 'array',
|
||||
'postgres_password' => 'encrypted',
|
||||
@@ -30,19 +33,17 @@ class StandalonePostgresql extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
$database->scheduledBackups()->delete();
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
@@ -50,6 +51,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');
|
||||
@@ -59,6 +69,17 @@ class StandalonePostgresql extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
|
||||
@@ -85,6 +106,11 @@ class StandalonePostgresql extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -179,18 +205,36 @@ class StandalonePostgresql extends BaseModel
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-postgresql';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
} else {
|
||||
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}",
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -227,4 +271,53 @@ class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
@@ -13,6 +14,8 @@ class StandaloneRedis extends BaseModel
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
@@ -25,19 +28,26 @@ class StandaloneRedis extends BaseModel
|
||||
'is_readonly' => true,
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($database) {
|
||||
$database->scheduledBackups()->delete();
|
||||
$storages = $database->persistentStorages()->get();
|
||||
$server = data_get($database, 'destination.server');
|
||||
if ($server) {
|
||||
foreach ($storages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
static::forceDeleting(function ($database) {
|
||||
$database->persistentStorages()->delete();
|
||||
$database->scheduledBackups()->delete();
|
||||
$database->environment_variables()->delete();
|
||||
$database->tags()->detach();
|
||||
});
|
||||
static::saving(function ($database) {
|
||||
if ($database->isDirty('status')) {
|
||||
$database->forceFill(['last_online_at' => now()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
@@ -66,6 +76,11 @@ class StandaloneRedis extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function isRunning()
|
||||
{
|
||||
return (bool) str($this->status)->contains('running');
|
||||
}
|
||||
|
||||
public function isExited()
|
||||
{
|
||||
return (bool) str($this->status)->startsWith('exited');
|
||||
@@ -85,6 +100,17 @@ class StandaloneRedis extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public function delete_volumes(Collection $persistentStorages)
|
||||
{
|
||||
if ($persistentStorages->count() === 0) {
|
||||
return;
|
||||
}
|
||||
$server = data_get($this, 'destination.server');
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
|
||||
public function realStatus()
|
||||
{
|
||||
return $this->getRawOriginal('status');
|
||||
@@ -179,13 +205,46 @@ class StandaloneRedis extends BaseModel
|
||||
return 'standalone-redis';
|
||||
}
|
||||
|
||||
public function get_db_url(bool $useInternal = false): string
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
if ($this->is_public && ! $useInternal) {
|
||||
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
} else {
|
||||
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function externalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getRedisVersion()
|
||||
{
|
||||
$image_parts = explode(':', $this->image);
|
||||
|
||||
return $image_parts[1] ?? '0.0';
|
||||
}
|
||||
|
||||
public function environment()
|
||||
@@ -222,4 +281,82 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error === 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function redisPassword(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
|
||||
if (! $password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $password->value;
|
||||
},
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public function redisUsername(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
|
||||
if (! $username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $username->value;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,22 +15,7 @@ class Subscription extends Model
|
||||
|
||||
public function type()
|
||||
{
|
||||
if (isLemon()) {
|
||||
$basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids'));
|
||||
$pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids'));
|
||||
$ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids'));
|
||||
|
||||
$subscription = $this->lemon_variant_id;
|
||||
if (in_array($subscription, $basic)) {
|
||||
return 'basic';
|
||||
}
|
||||
if (in_array($subscription, $pro)) {
|
||||
return 'pro';
|
||||
}
|
||||
if (in_array($subscription, $ultimate)) {
|
||||
return 'ultimate';
|
||||
}
|
||||
} elseif (isStripe()) {
|
||||
if (isStripe()) {
|
||||
if (! $this->stripe_plan_id) {
|
||||
return 'zero';
|
||||
}
|
||||
|
||||
@@ -7,7 +7,69 @@ use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Team model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the team.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the team.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The description of the team.'],
|
||||
'personal_team' => ['type' => 'boolean', 'description' => 'Whether the team is personal or not.'],
|
||||
'created_at' => ['type' => 'string', 'description' => 'The date and time the team was created.'],
|
||||
'updated_at' => ['type' => 'string', 'description' => 'The date and time the team was last updated.'],
|
||||
'smtp_enabled' => ['type' => 'boolean', 'description' => 'Whether SMTP is enabled or not.'],
|
||||
'smtp_from_address' => ['type' => 'string', 'description' => 'The email address to send emails from.'],
|
||||
'smtp_from_name' => ['type' => 'string', 'description' => 'The name to send emails from.'],
|
||||
'smtp_recipients' => ['type' => 'string', 'description' => 'The email addresses to send emails to.'],
|
||||
'smtp_host' => ['type' => 'string', 'description' => 'The SMTP host.'],
|
||||
'smtp_port' => ['type' => 'string', 'description' => 'The SMTP port.'],
|
||||
'smtp_encryption' => ['type' => 'string', 'description' => 'The SMTP encryption.'],
|
||||
'smtp_username' => ['type' => 'string', 'description' => 'The SMTP username.'],
|
||||
'smtp_password' => ['type' => 'string', 'description' => 'The SMTP password.'],
|
||||
'smtp_timeout' => ['type' => 'string', 'description' => 'The SMTP timeout.'],
|
||||
'smtp_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via SMTP.'],
|
||||
'smtp_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via SMTP.'],
|
||||
'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'],
|
||||
'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'],
|
||||
'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'],
|
||||
'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'],
|
||||
'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'],
|
||||
'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'],
|
||||
'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'],
|
||||
'discord_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Discord.'],
|
||||
'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'],
|
||||
'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'],
|
||||
'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'],
|
||||
'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'],
|
||||
'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'],
|
||||
'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'],
|
||||
'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'],
|
||||
'use_instance_email_settings' => ['type' => 'boolean', 'description' => 'Whether to use instance email settings or not.'],
|
||||
'telegram_enabled' => ['type' => 'boolean', 'description' => 'Whether Telegram is enabled or not.'],
|
||||
'telegram_token' => ['type' => 'string', 'description' => 'The Telegram token.'],
|
||||
'telegram_chat_id' => ['type' => 'string', 'description' => 'The Telegram chat ID.'],
|
||||
'telegram_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Telegram.'],
|
||||
'telegram_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Telegram.'],
|
||||
'telegram_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Telegram.'],
|
||||
'telegram_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Telegram.'],
|
||||
'telegram_notifications_test_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram test message thread ID.'],
|
||||
'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'],
|
||||
'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'],
|
||||
'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'],
|
||||
|
||||
'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'],
|
||||
'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'],
|
||||
'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'],
|
||||
'members' => new OA\Property(
|
||||
property: 'members',
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/User'),
|
||||
description: 'The members of the team.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class Team extends Model implements SendsDiscord, SendsEmail
|
||||
{
|
||||
use Notifiable;
|
||||
@@ -31,27 +93,22 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
static::deleting(function ($team) {
|
||||
$keys = $team->privateKeys;
|
||||
foreach ($keys as $key) {
|
||||
ray('Deleting key: '.$key->name);
|
||||
$key->delete();
|
||||
}
|
||||
$sources = $team->sources();
|
||||
foreach ($sources as $source) {
|
||||
ray('Deleting source: '.$source->name);
|
||||
$source->delete();
|
||||
}
|
||||
$tags = Tag::whereTeamId($team->id)->get();
|
||||
foreach ($tags as $tag) {
|
||||
ray('Deleting tag: '.$tag->name);
|
||||
$tag->delete();
|
||||
}
|
||||
$shared_variables = $team->environment_variables();
|
||||
foreach ($shared_variables as $shared_variable) {
|
||||
ray('Deleting team shared variable: '.$shared_variable->name);
|
||||
$shared_variable->delete();
|
||||
}
|
||||
$s3s = $team->s3s;
|
||||
foreach ($s3s as $s3) {
|
||||
ray('Deleting s3: '.$s3->name);
|
||||
$s3->delete();
|
||||
}
|
||||
});
|
||||
@@ -74,9 +131,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
{
|
||||
$recipients = data_get($notification, 'emails', null);
|
||||
if (is_null($recipients)) {
|
||||
$recipients = $this->members()->pluck('email')->toArray();
|
||||
|
||||
return $recipients;
|
||||
return $this->members()->pluck('email')->toArray();
|
||||
}
|
||||
|
||||
return explode(',', $recipients);
|
||||
@@ -105,8 +160,12 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
if (currentTeam()->id === 0 && isDev()) {
|
||||
return 9999999;
|
||||
}
|
||||
$team = Team::find(currentTeam()->id);
|
||||
if (! $team) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Team::find(currentTeam()->id)->limits['serverLimit'];
|
||||
return data_get($team, 'limits', 0);
|
||||
}
|
||||
|
||||
public function limits(): Attribute
|
||||
@@ -128,9 +187,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
} else {
|
||||
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
|
||||
}
|
||||
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
|
||||
|
||||
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
|
||||
return $serverLimit ?? 2;
|
||||
}
|
||||
|
||||
);
|
||||
@@ -190,9 +248,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
$sources = collect([]);
|
||||
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
|
||||
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
|
||||
$sources = $sources->merge($github_apps)->merge($gitlab_apps);
|
||||
|
||||
return $sources;
|
||||
return $sources->merge($github_apps)->merge($gitlab_apps);
|
||||
}
|
||||
|
||||
public function s3s()
|
||||
@@ -200,8 +257,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
return $this->hasMany(S3Storage::class)->where('is_usable', true);
|
||||
}
|
||||
|
||||
public function trialEnded()
|
||||
public function subscriptionEnded()
|
||||
{
|
||||
$this->subscription->update([
|
||||
'stripe_subscription_id' => null,
|
||||
'stripe_plan_id' => null,
|
||||
'stripe_cancel_at_period_end' => false,
|
||||
'stripe_invoice_paid' => false,
|
||||
'stripe_trial_already_ended' => false,
|
||||
]);
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => false,
|
||||
@@ -210,16 +274,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
|
||||
}
|
||||
}
|
||||
|
||||
public function trialEndedButSubscribed()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
$server->settings()->update([
|
||||
'is_usable' => true,
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function isAnyNotificationEnabled()
|
||||
{
|
||||
if (isCloud()) {
|
||||
|
||||
@@ -20,11 +20,16 @@ class TeamInvitation extends Model
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return TeamInvitation::whereTeamId(currentTeam()->id);
|
||||
}
|
||||
|
||||
public function isValid()
|
||||
{
|
||||
$createdAt = $this->created_at;
|
||||
$diff = $createdAt->diffInMinutes(now());
|
||||
if ($diff <= config('constants.invitation.link.expiration')) {
|
||||
$diff = $createdAt->diffInDays(now());
|
||||
if ($diff <= config('constants.invitation.link.expiration_days')) {
|
||||
return true;
|
||||
} else {
|
||||
$this->delete();
|
||||
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
@@ -17,7 +18,23 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Laravel\Sanctum\NewAccessToken;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'User model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The user identifier in the database.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The user name.'],
|
||||
'email' => ['type' => 'string', 'description' => 'The user email.'],
|
||||
'email_verified_at' => ['type' => 'string', 'description' => 'The date when the user email was verified.'],
|
||||
'created_at' => ['type' => 'string', 'description' => 'The date when the user was created.'],
|
||||
'updated_at' => ['type' => 'string', 'description' => 'The date when the user was updated.'],
|
||||
'two_factor_confirmed_at' => ['type' => 'string', 'description' => 'The date when the user two factor was confirmed.'],
|
||||
'force_password_reset' => ['type' => 'boolean', 'description' => 'The flag to force the user to reset the password.'],
|
||||
'marketing_emails' => ['type' => 'boolean', 'description' => 'The flag to receive marketing emails.'],
|
||||
],
|
||||
)]
|
||||
class User extends Authenticatable implements SendsEmail
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable;
|
||||
@@ -104,7 +121,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function sendVerificationEmail()
|
||||
{
|
||||
$mail = new MailMessage();
|
||||
$mail = new MailMessage;
|
||||
$url = Url::temporarySignedRoute(
|
||||
'verify.verify',
|
||||
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
|
||||
@@ -142,7 +159,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function isAdminFromSession()
|
||||
{
|
||||
if (auth()->user()->id === 0) {
|
||||
if (Auth::id() === 0) {
|
||||
return true;
|
||||
}
|
||||
$teams = $this->teams()->get();
|
||||
@@ -162,8 +179,12 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function isInstanceAdmin()
|
||||
{
|
||||
$found_root_team = auth()->user()->teams->filter(function ($team) {
|
||||
$found_root_team = Auth::user()->teams->filter(function ($team) {
|
||||
if ($team->id == 0) {
|
||||
if (! Auth::user()->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -175,9 +196,9 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function currentTeam()
|
||||
{
|
||||
return Cache::remember('team:'.auth()->user()->id, 3600, function () {
|
||||
if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) {
|
||||
return auth()->user()->teams[0];
|
||||
return Cache::remember('team:'.Auth::id(), 3600, function () {
|
||||
if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
|
||||
return Auth::user()->teams[0];
|
||||
}
|
||||
|
||||
return Team::find(session('currentTeam')->id);
|
||||
@@ -186,7 +207,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function otherTeams()
|
||||
{
|
||||
return auth()->user()->teams->filter(function ($team) {
|
||||
return Auth::user()->teams->filter(function ($team) {
|
||||
return $team->id != currentTeam()->id;
|
||||
});
|
||||
}
|
||||
@@ -196,7 +217,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
if (data_get($this, 'pivot')) {
|
||||
return $this->pivot->role;
|
||||
}
|
||||
$user = auth()->user()->teams->where('id', currentTeam()->id)->first();
|
||||
$user = Auth::user()->teams->where('id', currentTeam()->id)->first();
|
||||
|
||||
return data_get($user, 'pivot.role');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user