mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-27 20:49:32 +00:00
Compare commits
166 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ffebd71f3 | ||
|
|
2f286a6595 | ||
|
|
13701f6030 | ||
|
|
91f224ddea | ||
|
|
8cffae10b2 | ||
|
|
1158b2f4db | ||
|
|
f542bcf428 | ||
|
|
22178df8ae | ||
|
|
acfe1daf9b | ||
|
|
fb3f71881f | ||
|
|
0f7546a4dc | ||
|
|
6ccafacc87 | ||
|
|
852c2df12e | ||
|
|
4c752951ab | ||
|
|
f32191b889 | ||
|
|
31251ef6cb | ||
|
|
e9365aa09b | ||
|
|
e5c860319f | ||
|
|
ceedd5225f | ||
|
|
61efdfb7c1 | ||
|
|
fcef5d902e | ||
|
|
2073b8949b | ||
|
|
5c59a752e3 | ||
|
|
caf6e3a23e | ||
|
|
8eb1c4da46 | ||
|
|
bab2f391ed | ||
|
|
131c6df82a | ||
|
|
9f44d0c47a | ||
|
|
dff7ed5b7b | ||
|
|
54b6472f3b | ||
|
|
04a36e5c90 | ||
|
|
b2f851272b | ||
|
|
778915599f | ||
|
|
a6f9527157 | ||
|
|
db976709c7 | ||
|
|
7e4947ba07 | ||
|
|
e2578a7dd0 | ||
|
|
7d028b15f5 | ||
|
|
d240bfda8b | ||
|
|
d59ec2548a | ||
|
|
e0cefc787a | ||
|
|
0496198d0f | ||
|
|
cbf7f1fa41 | ||
|
|
66587d864a | ||
|
|
b106705c8d | ||
|
|
5cc22a8271 | ||
|
|
cd22863a8c | ||
|
|
118a02f70d | ||
|
|
862177d61a | ||
|
|
41280aa780 | ||
|
|
3a987c8a6e | ||
|
|
29ca461a9a | ||
|
|
d48b72c160 | ||
|
|
ce49f26c53 | ||
|
|
02989678be | ||
|
|
810b55163c | ||
|
|
ac13ba0957 | ||
|
|
a3e088199f | ||
|
|
42ee4ca032 | ||
|
|
17deff4d86 | ||
|
|
8b6323b906 | ||
|
|
a696b5271a | ||
|
|
31959f26d4 | ||
|
|
45d2f80f69 | ||
|
|
53975fcf61 | ||
|
|
76296c1f19 | ||
|
|
c25baf69e1 | ||
|
|
f952512615 | ||
|
|
c6557eada8 | ||
|
|
2c2d74c0d6 | ||
|
|
028a2eb275 | ||
|
|
ce7fad5bef | ||
|
|
cd7852e4f9 | ||
|
|
7b022a2482 | ||
|
|
12d9b6538b | ||
|
|
335788c2d6 | ||
|
|
2352e4a71d | ||
|
|
dc03179bd1 | ||
|
|
cc72f416e8 | ||
|
|
3b67d0a8de | ||
|
|
0135ba7e89 | ||
|
|
a28a28cd23 | ||
|
|
b52680a2d8 | ||
|
|
0670e6c1d6 | ||
|
|
c3882b75c1 | ||
|
|
64b6f86a36 | ||
|
|
b9efc22253 | ||
|
|
e3d9eb0154 | ||
|
|
66f3967479 | ||
|
|
c54439e84c | ||
|
|
db4a4c74fc | ||
|
|
5c7ef80219 | ||
|
|
243d1c06fc | ||
|
|
ef25f7d800 | ||
|
|
45640ffdb1 | ||
|
|
378291b209 | ||
|
|
3583e552f1 | ||
|
|
d7dfeaf988 | ||
|
|
7fe5eca661 | ||
|
|
0dff57e69f | ||
|
|
f4803ad58b | ||
|
|
2d7bbbe300 | ||
|
|
928b68043b | ||
|
|
b21add0210 | ||
|
|
c41ffd6bfb | ||
|
|
b4874c7df3 | ||
|
|
c505a6ce9c | ||
|
|
706e4b13ee | ||
|
|
4af471ee31 | ||
|
|
87062e4e22 | ||
|
|
500ba0fab8 | ||
|
|
1c72c127d5 | ||
|
|
69bb4ae5ee | ||
|
|
5f8b8bd730 | ||
|
|
44f6d93639 | ||
|
|
e35b8a0f96 | ||
|
|
b26e23e7c3 | ||
|
|
e6f7e32037 | ||
|
|
1c386db41d | ||
|
|
085b655d9f | ||
|
|
2788fcf4e1 | ||
|
|
d058e04213 | ||
|
|
066f171163 | ||
|
|
2001be07d0 | ||
|
|
39552cc42f | ||
|
|
7f5d7e0eb0 | ||
|
|
0eda49b104 | ||
|
|
636995d0e4 | ||
|
|
4c0623f022 | ||
|
|
3e2e1080f5 | ||
|
|
3f866a07d8 | ||
|
|
23571ae104 | ||
|
|
c1710c8f7b | ||
|
|
d4d2cc71a0 | ||
|
|
8d86d53292 | ||
|
|
fae97e4dee | ||
|
|
8d0c3abf2e | ||
|
|
d396f649df | ||
|
|
ec21155c9e | ||
|
|
58111f53b9 | ||
|
|
2cbe1e8489 | ||
|
|
10e5a58b9e | ||
|
|
6f886e8b6f | ||
|
|
f96a91eb31 | ||
|
|
65a1961722 | ||
|
|
c5a932ab88 | ||
|
|
d1e10dacc0 | ||
|
|
96327af838 | ||
|
|
1cb6d594d0 | ||
|
|
16261fc36e | ||
|
|
cff694b0c4 | ||
|
|
97fd56b9e4 | ||
|
|
72cfa3e7b0 | ||
|
|
3cf41e1e23 | ||
|
|
2a7a63a672 | ||
|
|
7fb9e672cf | ||
|
|
9012f6b953 | ||
|
|
407eba8b76 | ||
|
|
68f6ab5796 | ||
|
|
3dd36a2271 | ||
|
|
7f69eb3c2e | ||
|
|
6ccbf911b2 | ||
|
|
5c77cec68f | ||
|
|
25a0489f7f | ||
|
|
5e27b88bef | ||
|
|
ccb972dcb9 |
@@ -4,3 +4,7 @@ APP_KEY=
|
||||
|
||||
DB_PASSWORD=
|
||||
REDIS_PASSWORD=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
|
||||
@@ -11,19 +11,34 @@ class StopApplication
|
||||
public function handle(Application $application)
|
||||
{
|
||||
$server = $application->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
["docker rm -f {$containerName}"],
|
||||
$server
|
||||
);
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
foreach ($containers as $container) {
|
||||
$containerName = data_get($container, 'Names');
|
||||
if ($containerName) {
|
||||
instant_remote_process(
|
||||
["docker rm -f {$containerName}"],
|
||||
$server
|
||||
);
|
||||
}
|
||||
}
|
||||
// TODO: make notification for application
|
||||
// $application->environment->project->team->notify(new StatusChanged($application));
|
||||
}
|
||||
// Delete Preview Deployments
|
||||
$previewDeployments = $application->previews;
|
||||
foreach ($previewDeployments as $previewDeployment) {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
|
||||
}
|
||||
}
|
||||
// TODO: make notification for application
|
||||
// $application->environment->project->team->notify(new StatusChanged($application));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,35 +17,42 @@ class CheckProxy
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$status = getContainerStatus($server, 'coolify-proxy');
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
if ($server->isSwarm()) {
|
||||
$status = getContainerStatus($server, 'coolify-proxy_traefik');
|
||||
$server->proxy->set('status', $status);
|
||||
$server->save();
|
||||
return false;
|
||||
}
|
||||
$ip = $server->ip;
|
||||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
} else {
|
||||
$status = getContainerStatus($server, 'coolify-proxy');
|
||||
if ($status === 'running') {
|
||||
$server->proxy->set('status', 'running');
|
||||
$server->save();
|
||||
return false;
|
||||
}
|
||||
$ip = $server->ip;
|
||||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
|
||||
$connection80 = @fsockopen($ip, '80');
|
||||
$connection443 = @fsockopen($ip, '443');
|
||||
$port80 = is_resource($connection80) && fclose($connection80);
|
||||
$port443 = is_resource($connection443) && fclose($connection443);
|
||||
if ($port80) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
$connection80 = @fsockopen($ip, '80');
|
||||
$connection443 = @fsockopen($ip, '443');
|
||||
$port80 = is_resource($connection80) && fclose($connection80);
|
||||
$port443 = is_resource($connection443) && fclose($connection443);
|
||||
if ($port80) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($port443) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
if ($port443) {
|
||||
if ($fromUI) {
|
||||
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class StartProxy
|
||||
public function handle(Server $server, bool $async = true): string|Activity
|
||||
{
|
||||
try {
|
||||
|
||||
$proxyType = $server->proxyType();
|
||||
$commands = collect([]);
|
||||
$proxy_path = get_proxy_path();
|
||||
@@ -24,18 +25,29 @@ class StartProxy
|
||||
$docker_compose_yml_base64 = base64_encode($configuration);
|
||||
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
|
||||
$server->save();
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path && cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
"echo 'Stopping existing coolify-proxy.'",
|
||||
"docker compose down -v --remove-orphans > /dev/null 2>&1",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Proxy started successfully.'"
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
if ($server->isSwarm()) {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path && cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
"cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
|
||||
"echo 'Proxy started successfully.'"
|
||||
]);
|
||||
} else {
|
||||
$commands = $commands->merge([
|
||||
"mkdir -p $proxy_path && cd $proxy_path",
|
||||
"echo 'Creating required Docker Compose file.'",
|
||||
"echo 'Pulling docker image.'",
|
||||
'docker compose pull',
|
||||
"echo 'Stopping existing coolify-proxy.'",
|
||||
"docker compose down -v --remove-orphans > /dev/null 2>&1",
|
||||
"echo 'Starting coolify-proxy.'",
|
||||
'docker compose up -d --remove-orphans',
|
||||
"echo 'Proxy started successfully.'"
|
||||
]);
|
||||
$commands = $commands->merge(connectProxyToNetworks($server));
|
||||
}
|
||||
|
||||
if ($async) {
|
||||
$activity = remote_process($commands, $server);
|
||||
return $activity;
|
||||
@@ -46,11 +58,9 @@ class StartProxy
|
||||
$server->save();
|
||||
return 'OK';
|
||||
}
|
||||
} catch(\Throwable $e) {
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class InstallDocker
|
||||
if (!$supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
|
||||
}
|
||||
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type);
|
||||
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type);
|
||||
$dockerVersion = '24.0';
|
||||
$config = base64_encode('{
|
||||
"log-driver": "json-file",
|
||||
@@ -44,24 +44,30 @@ class InstallDocker
|
||||
"ls -l /tmp"
|
||||
]);
|
||||
} else {
|
||||
if ($supported_os_type === 'debian') {
|
||||
if ($supported_os_type->contains('debian')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || apt-get update",
|
||||
"command -v jq >/dev/null || apt install -y jq",
|
||||
"command -v jq >/dev/null || apt-get update -y",
|
||||
"command -v jq >/dev/null || apt install -y curl wget git jq",
|
||||
|
||||
]);
|
||||
} else if ($supported_os_type === 'rhel') {
|
||||
} else if ($supported_os_type->contains('rhel')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || dnf install -y jq",
|
||||
"command -v jq >/dev/null || dnf install -y curl wget git jq",
|
||||
]);
|
||||
} else if ($supported_os_type->contains('sles')) {
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Prerequisites...'",
|
||||
"command -v jq >/dev/null || zypper update -y",
|
||||
"command -v jq >/dev/null || zypper install -y curl wget git jq",
|
||||
]);
|
||||
} else {
|
||||
throw new \Exception('Unsupported OS');
|
||||
}
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
|
||||
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
|
||||
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
|
||||
@@ -70,10 +76,20 @@ class InstallDocker
|
||||
"echo 'Restarting Docker Engine...'",
|
||||
"systemctl enable docker >/dev/null 2>&1 || true",
|
||||
"systemctl restart docker",
|
||||
"echo 'Creating default Docker network (coolify)...'",
|
||||
"docker network create --attachable coolify >/dev/null 2>&1 || true",
|
||||
"echo 'Done!'"
|
||||
]);
|
||||
if ($server->isSwarm()) {
|
||||
$command = $command->merge([
|
||||
"docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true",
|
||||
]);
|
||||
} else {
|
||||
$command = $command->merge([
|
||||
"docker network create --attachable coolify >/dev/null 2>&1 || true",
|
||||
]);
|
||||
$command = $command->merge([
|
||||
"echo 'Done!'",
|
||||
]);
|
||||
}
|
||||
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class InstallLogDrain
|
||||
$type = 'highlight';
|
||||
} else if ($server->settings->is_logdrain_axiom_enabled) {
|
||||
$type = 'axiom';
|
||||
} else if ($server->settings->is_logdrain_custom_enabled) {
|
||||
$type = 'custom';
|
||||
} else {
|
||||
$type = 'none';
|
||||
}
|
||||
@@ -114,15 +116,23 @@ class InstallLogDrain
|
||||
json_date_format iso8601
|
||||
tls On
|
||||
");
|
||||
} else if ($type === 'custom') {
|
||||
if (!$server->settings->is_logdrain_custom_enabled) {
|
||||
throw new \Exception('Custom log drain is not enabled.');
|
||||
}
|
||||
$config = base64_encode($server->settings->logdrain_custom_config);
|
||||
$parsers = base64_encode($server->settings->logdrain_custom_config_parser);
|
||||
} else {
|
||||
throw new \Exception('Unknown log drain type.');
|
||||
}
|
||||
$parsers = base64_encode("
|
||||
if ($type !== 'custom') {
|
||||
$parsers = base64_encode("
|
||||
[PARSER]
|
||||
Name empty_line_skipper
|
||||
Format regex
|
||||
Regex /^(?!\s*$).+/
|
||||
Name empty_line_skipper
|
||||
Format regex
|
||||
Regex /^(?!\s*$).+/
|
||||
");
|
||||
}
|
||||
$compose = base64_encode("
|
||||
services:
|
||||
coolify-log-drain:
|
||||
@@ -179,6 +189,12 @@ Files:
|
||||
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
|
||||
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
|
||||
];
|
||||
} else if ($type === 'custom') {
|
||||
$add_envs_command = [
|
||||
"touch $config_path/.env"
|
||||
];
|
||||
} else {
|
||||
throw new \Exception('Unknown log drain type.');
|
||||
}
|
||||
$restart_command = [
|
||||
"echo 'Stopping old Fluent Bit'",
|
||||
|
||||
@@ -16,13 +16,13 @@ class StartService
|
||||
$commands[] = "cd " . $service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
$commands[] = "echo 'Creating Docker network.'";
|
||||
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
|
||||
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null 2>&1 || true";
|
||||
$commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
|
||||
$commands[] = "echo 'Pulling images.'";
|
||||
$commands[] = "docker compose pull";
|
||||
$commands[] = "echo 'Starting containers.'";
|
||||
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
|
||||
$commands[] = "docker compose up -d --remove-orphans --force-recreate --build";
|
||||
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
|
||||
$compose = data_get($service,'docker_compose',[]);
|
||||
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
|
||||
foreach($serviceNames as $serviceName => $serviceConfig){
|
||||
|
||||
@@ -30,20 +30,21 @@ class Init extends Command
|
||||
$this->alive();
|
||||
$cleanup = $this->option('cleanup');
|
||||
if ($cleanup) {
|
||||
echo "Running cleanup\n";
|
||||
$this->cleanup_stucked_resources();
|
||||
$this->cleanup_ssh();
|
||||
// $this->cleanup_ssh();
|
||||
}
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanup_stucked_helper_containers();
|
||||
}
|
||||
private function cleanup_stucked_helper_containers() {
|
||||
private function cleanup_stucked_helper_containers()
|
||||
{
|
||||
$servers = Server::all();
|
||||
foreach ($servers as $server) {
|
||||
if ($server->isFunctional()) {
|
||||
CleanupHelperContainersJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private function alive()
|
||||
{
|
||||
@@ -62,21 +63,23 @@ class Init extends Command
|
||||
echo "Error in alive: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
private function cleanup_ssh()
|
||||
{
|
||||
try {
|
||||
$files = Storage::allFiles('ssh/keys');
|
||||
foreach ($files as $file) {
|
||||
Storage::delete($file);
|
||||
}
|
||||
$files = Storage::allFiles('ssh/mux');
|
||||
foreach ($files as $file) {
|
||||
Storage::delete($file);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleaning ssh: {$e->getMessage()}\n";
|
||||
}
|
||||
}
|
||||
// private function cleanup_ssh()
|
||||
// {
|
||||
|
||||
// TODO: it will cleanup id.root@host.docker.internal
|
||||
// try {
|
||||
// $files = Storage::allFiles('ssh/keys');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// $files = Storage::allFiles('ssh/mux');
|
||||
// foreach ($files as $file) {
|
||||
// Storage::delete($file);
|
||||
// }
|
||||
// } catch (\Throwable $e) {
|
||||
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
|
||||
// }
|
||||
// }
|
||||
private function cleanup_in_progress_application_deployments()
|
||||
{
|
||||
// Cleanup any failed deployments
|
||||
@@ -98,15 +101,15 @@ class Init extends Command
|
||||
$applications = Application::all();
|
||||
foreach ($applications as $application) {
|
||||
if (!data_get($application, 'environment')) {
|
||||
ray('Application without environment', $application->name);
|
||||
$application->delete();
|
||||
}
|
||||
if (!data_get($application, 'destination.server')) {
|
||||
ray('Application without server', $application->name);
|
||||
echo 'Application without environment' . $application->name . 'deleting\n';
|
||||
$application->delete();
|
||||
}
|
||||
if (!$application->destination()) {
|
||||
ray('Application without destination', $application->name);
|
||||
echo 'Application without destination' . $application->name . 'deleting\n';
|
||||
$application->delete();
|
||||
}
|
||||
if (!data_get($application, 'destination.server')) {
|
||||
echo 'Application without server' . $application->name . 'deleting\n';
|
||||
$application->delete();
|
||||
}
|
||||
}
|
||||
@@ -117,15 +120,15 @@ class Init extends Command
|
||||
$postgresqls = StandalonePostgresql::all();
|
||||
foreach ($postgresqls as $postgresql) {
|
||||
if (!data_get($postgresql, 'environment')) {
|
||||
ray('Postgresql without environment', $postgresql->name);
|
||||
$postgresql->delete();
|
||||
}
|
||||
if (!data_get($postgresql, 'destination.server')) {
|
||||
ray('Postgresql without server', $postgresql->name);
|
||||
echo 'Postgresql without environment' . $postgresql->name . 'deleting\n';
|
||||
$postgresql->delete();
|
||||
}
|
||||
if (!$postgresql->destination()) {
|
||||
ray('Postgresql without destination', $postgresql->name);
|
||||
echo 'Postgresql without destination' . $postgresql->name . 'deleting\n';
|
||||
$postgresql->delete();
|
||||
}
|
||||
if (!data_get($postgresql, 'destination.server')) {
|
||||
echo 'Postgresql without server' . $postgresql->name . 'deleting\n';
|
||||
$postgresql->delete();
|
||||
}
|
||||
}
|
||||
@@ -136,15 +139,15 @@ class Init extends Command
|
||||
$redis = StandaloneRedis::all();
|
||||
foreach ($redis as $redis) {
|
||||
if (!data_get($redis, 'environment')) {
|
||||
ray('Redis without environment', $redis->name);
|
||||
$redis->delete();
|
||||
}
|
||||
if (!data_get($redis, 'destination.server')) {
|
||||
ray('Redis without server', $redis->name);
|
||||
echo 'Redis without environment' . $redis->name . 'deleting\n';
|
||||
$redis->delete();
|
||||
}
|
||||
if (!$redis->destination()) {
|
||||
ray('Redis without destination', $redis->name);
|
||||
echo 'Redis without destination' . $redis->name . 'deleting\n';
|
||||
$redis->delete();
|
||||
}
|
||||
if (!data_get($redis, 'destination.server')) {
|
||||
echo 'Redis without server' . $redis->name . 'deleting\n';
|
||||
$redis->delete();
|
||||
}
|
||||
}
|
||||
@@ -156,15 +159,15 @@ class Init extends Command
|
||||
$mongodbs = StandaloneMongodb::all();
|
||||
foreach ($mongodbs as $mongodb) {
|
||||
if (!data_get($mongodb, 'environment')) {
|
||||
ray('Mongodb without environment', $mongodb->name);
|
||||
$mongodb->delete();
|
||||
}
|
||||
if (!data_get($mongodb, 'destination.server')) {
|
||||
ray('Mongodb without server', $mongodb->name);
|
||||
echo 'Mongodb without environment' . $mongodb->name . 'deleting\n';
|
||||
$mongodb->delete();
|
||||
}
|
||||
if (!$mongodb->destination()) {
|
||||
ray('Mongodb without destination', $mongodb->name);
|
||||
echo 'Mongodb without destination' . $mongodb->name . 'deleting\n';
|
||||
$mongodb->delete();
|
||||
}
|
||||
if (!data_get($mongodb, 'destination.server')) {
|
||||
echo 'Mongodb without server' . $mongodb->name . 'deleting\n';
|
||||
$mongodb->delete();
|
||||
}
|
||||
}
|
||||
@@ -176,15 +179,15 @@ class Init extends Command
|
||||
$mysqls = StandaloneMysql::all();
|
||||
foreach ($mysqls as $mysql) {
|
||||
if (!data_get($mysql, 'environment')) {
|
||||
ray('Mysql without environment', $mysql->name);
|
||||
$mysql->delete();
|
||||
}
|
||||
if (!data_get($mysql, 'destination.server')) {
|
||||
ray('Mysql without server', $mysql->name);
|
||||
echo 'Mysql without environment' . $mysql->name . 'deleting\n';
|
||||
$mysql->delete();
|
||||
}
|
||||
if (!$mysql->destination()) {
|
||||
ray('Mysql without destination', $mysql->name);
|
||||
echo 'Mysql without destination' . $mysql->name . 'deleting\n';
|
||||
$mysql->delete();
|
||||
}
|
||||
if (!data_get($mysql, 'destination.server')) {
|
||||
echo 'Mysql without server' . $mysql->name . 'deleting\n';
|
||||
$mysql->delete();
|
||||
}
|
||||
}
|
||||
@@ -196,15 +199,15 @@ class Init extends Command
|
||||
$mariadbs = StandaloneMariadb::all();
|
||||
foreach ($mariadbs as $mariadb) {
|
||||
if (!data_get($mariadb, 'environment')) {
|
||||
ray('Mariadb without environment', $mariadb->name);
|
||||
$mariadb->delete();
|
||||
}
|
||||
if (!data_get($mariadb, 'destination.server')) {
|
||||
ray('Mariadb without server', $mariadb->name);
|
||||
echo 'Mariadb without environment' . $mariadb->name . 'deleting\n';
|
||||
$mariadb->delete();
|
||||
}
|
||||
if (!$mariadb->destination()) {
|
||||
ray('Mariadb without destination', $mariadb->name);
|
||||
echo 'Mariadb without destination' . $mariadb->name . 'deleting\n';
|
||||
$mariadb->delete();
|
||||
}
|
||||
if (!data_get($mariadb, 'destination.server')) {
|
||||
echo 'Mariadb without server' . $mariadb->name . 'deleting\n';
|
||||
$mariadb->delete();
|
||||
}
|
||||
}
|
||||
@@ -216,15 +219,15 @@ class Init extends Command
|
||||
$services = Service::all();
|
||||
foreach ($services as $service) {
|
||||
if (!data_get($service, 'environment')) {
|
||||
ray('Service without environment', $service->name);
|
||||
$service->delete();
|
||||
}
|
||||
if (!data_get($service, 'server')) {
|
||||
ray('Service without server', $service->name);
|
||||
echo 'Service without environment' . $service->name . 'deleting\n';
|
||||
$service->delete();
|
||||
}
|
||||
if (!$service->destination()) {
|
||||
ray('Service without destination', $service->name);
|
||||
echo 'Service without destination' . $service->name . 'deleting\n';
|
||||
$service->delete();
|
||||
}
|
||||
if (!data_get($service, 'server')) {
|
||||
echo 'Service without server' . $service->name . 'deleting\n';
|
||||
$service->delete();
|
||||
}
|
||||
}
|
||||
@@ -235,7 +238,7 @@ class Init extends Command
|
||||
$serviceApplications = ServiceApplication::all();
|
||||
foreach ($serviceApplications as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
ray('ServiceApplication without service', $service->name);
|
||||
echo 'ServiceApplication without service' . $service->name . 'deleting\n';
|
||||
$service->delete();
|
||||
}
|
||||
}
|
||||
@@ -246,7 +249,7 @@ class Init extends Command
|
||||
$serviceDatabases = ServiceDatabase::all();
|
||||
foreach ($serviceDatabases as $service) {
|
||||
if (!data_get($service, 'service')) {
|
||||
ray('ServiceDatabase without service', $service->name);
|
||||
echo 'ServiceDatabase without service' . $service->name . 'deleting\n';
|
||||
$service->delete();
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Console/Commands/RootChangeEmail.php
Normal file
41
app/Console/Commands/RootChangeEmail.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RootChangeEmail extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'root:change-email';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Change Root Email';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//
|
||||
$this->info('You are about to change the root user\'s email.');
|
||||
$email = $this->ask('Give me a new email for root user');
|
||||
$this->info('Updating root email...');
|
||||
try {
|
||||
User::find(0)->update(['email' => $email]);
|
||||
$this->info('Root user\'s email updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to update root user\'s email.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,14 @@ use Illuminate\Support\Facades\Hash;
|
||||
|
||||
use function Laravel\Prompts\password;
|
||||
|
||||
class UsersResetRoot extends Command
|
||||
class RootResetPassword extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'users:reset-root';
|
||||
protected $signature = 'root:reset-password';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@@ -12,21 +12,21 @@ use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\select;
|
||||
|
||||
class ResourcesDelete extends Command
|
||||
class ServicesDelete extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'resources:delete';
|
||||
protected $signature = 'services:delete';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Delete a resource from the database';
|
||||
protected $description = 'Delete a service from the database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
@@ -34,7 +34,7 @@ class ResourcesDelete extends Command
|
||||
public function handle()
|
||||
{
|
||||
$resource = select(
|
||||
'What resource do you want to delete?',
|
||||
'What service do you want to delete?',
|
||||
['Application', 'Database', 'Service', 'Server'],
|
||||
);
|
||||
if ($resource === 'Application') {
|
||||
@@ -26,7 +26,7 @@ class ServicesGenerate extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
ray()->clearAll();
|
||||
// ray()->clearAll();
|
||||
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
|
||||
$files = array_filter($files, function ($file) {
|
||||
return strpos($file, '.yaml') !== false;
|
||||
|
||||
@@ -71,6 +71,15 @@ class SyncBunny extends Command
|
||||
]);
|
||||
});
|
||||
try {
|
||||
if (!$only_template && !$only_version) {
|
||||
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
|
||||
}
|
||||
if ($only_template) {
|
||||
$this->info('About to sync service-templates.json to BunnyCDN.');
|
||||
}
|
||||
if ($only_version) {
|
||||
$this->info('About to sync versions.json to BunnyCDN.');
|
||||
}
|
||||
$confirmed = confirm('Are you sure you want to sync?');
|
||||
if (!$confirmed) {
|
||||
return;
|
||||
|
||||
28
app/Events/TestEvent.php
Normal file
28
app/Events/TestEvent.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TestEvent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
public $teamId;
|
||||
public function __construct()
|
||||
{
|
||||
$this->teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel("custom.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ class ProjectController extends Controller
|
||||
$database = create_standalone_mongodb($environment->id, $destination_uuid);
|
||||
} else if ($type->value() === 'mysql') {
|
||||
$database = create_standalone_mysql($environment->id, $destination_uuid);
|
||||
}else if ($type->value() === 'mariadb') {
|
||||
} else if ($type->value() === 'mariadb') {
|
||||
$database = create_standalone_mariadb($environment->id, $destination_uuid);
|
||||
}
|
||||
return redirect()->route('project.database.configuration', [
|
||||
@@ -104,27 +104,7 @@ class ProjectController extends Controller
|
||||
$generatedValue = $value;
|
||||
if ($value->contains('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
// TODO: make it shared with Service.php
|
||||
switch ($command->value()) {
|
||||
case 'PASSWORD':
|
||||
$generatedValue = Str::password(symbols: false);
|
||||
break;
|
||||
case 'PASSWORD_64':
|
||||
$generatedValue = Str::password(length: 64, symbols: false);
|
||||
break;
|
||||
case 'BASE64_64':
|
||||
$generatedValue = Str::random(64);
|
||||
break;
|
||||
case 'BASE64_128':
|
||||
$generatedValue = Str::random(128);
|
||||
break;
|
||||
case 'BASE64':
|
||||
$generatedValue = Str::random(32);
|
||||
break;
|
||||
case 'USER':
|
||||
$generatedValue = Str::random(16);
|
||||
break;
|
||||
}
|
||||
$generatedValue = generateEnvValue($command->value());
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
@@ -137,7 +117,7 @@ class ProjectController extends Controller
|
||||
}
|
||||
$service->parse(isNew: true);
|
||||
|
||||
return redirect()->route('project.service', [
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
'project_uuid' => $project->uuid,
|
||||
|
||||
@@ -31,6 +31,8 @@ class Index extends Component
|
||||
public ?string $remoteServerHost = null;
|
||||
public ?int $remoteServerPort = 22;
|
||||
public ?string $remoteServerUser = 'root';
|
||||
public bool $isSwarmManager = false;
|
||||
public bool $isCloudflareTunnel = false;
|
||||
public ?Server $createdServer = null;
|
||||
|
||||
public Collection $projects;
|
||||
@@ -182,7 +184,10 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
'private_key_id' => $this->createdPrivateKey->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
$this->createdServer->save();
|
||||
$this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
|
||||
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
|
||||
$this->createdServer->settings->save();
|
||||
$this->createdServer->addInitialNetwork();
|
||||
$this->validateServer();
|
||||
}
|
||||
public function validateServer()
|
||||
@@ -197,6 +202,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
$this->serverReachable = false;
|
||||
$this->createdServer->delete();
|
||||
return handleError(error: $e, livewire: $this);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ class Dashboard extends Component
|
||||
{
|
||||
public $projects = [];
|
||||
public $servers = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->servers = Server::ownedByCurrentTeam()->get();
|
||||
|
||||
29
app/Http/Livewire/Modal/EditCompose.php
Normal file
29
app/Http/Livewire/Modal/EditCompose.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Modal;
|
||||
|
||||
use App\Models\Service;
|
||||
use Livewire\Component;
|
||||
use LivewireUI\Modal\ModalComponent;
|
||||
|
||||
class EditCompose extends ModalComponent
|
||||
{
|
||||
public Service $service;
|
||||
public $serviceId;
|
||||
protected $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
];
|
||||
public function mount() {
|
||||
$this->service = Service::find($this->serviceId);
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.modal.edit-compose');
|
||||
}
|
||||
public function submit() {
|
||||
$this->emit('warning', "Saving new docker compose...");
|
||||
$this->emit('saveCompose', $this->service->docker_compose_raw);
|
||||
$this->closeModal();
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class Advanced extends Component
|
||||
];
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->application->settings->is_log_drain_enabled) {
|
||||
if ($this->application->isLogDrainEnabled()) {
|
||||
if (!$this->application->destination->server->isLogDrainEnabled()) {
|
||||
$this->application->settings->is_log_drain_enabled = false;
|
||||
$this->emit('error', 'Log drain is not enabled on this server.');
|
||||
|
||||
110
app/Http/Livewire/Project/Application/Command.php
Normal file
110
app/Http/Livewire/Project/Application/Command.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Project\Application;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Livewire\Component;
|
||||
|
||||
class Command extends Component
|
||||
{
|
||||
public string $command;
|
||||
public string $container;
|
||||
public $containers;
|
||||
public $parameters;
|
||||
public $resource;
|
||||
public string $type;
|
||||
public string $workDir = '';
|
||||
public Server $server;
|
||||
public $servers = [];
|
||||
|
||||
protected $rules = [
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
'command' => 'required',
|
||||
'workDir' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->server = $this->resource->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
}
|
||||
} else if (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->type = 'database';
|
||||
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$this->container = $this->resource->uuid;
|
||||
$this->containers->push($this->container);
|
||||
} else if (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
if (str(data_get($application, 'status'))->contains('running')) {
|
||||
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if (str(data_get($database, 'status'))->contains('running')) {
|
||||
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
}
|
||||
});
|
||||
|
||||
$this->server = $this->resource->server;
|
||||
}
|
||||
if ($this->containers->count() > 1) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
if (!empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$this->container} {$this->command}";
|
||||
} else {
|
||||
$exec = "docker exec {$this->container} {$this->command}";
|
||||
}
|
||||
$activity = remote_process([$exec], $this->server, ignore_errors: true);
|
||||
$this->emit('newMonitorActivity', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.execute-container-command');
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class Configuration extends Component
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->application = $application;
|
||||
$mainServer = $application->destination->server;
|
||||
$mainServer = $this->application->destination->server;
|
||||
$servers = Server::ownedByCurrentTeam()->get();
|
||||
$this->servers = $servers->filter(function ($server) use ($mainServer) {
|
||||
return $server->id != $mainServer->id;
|
||||
|
||||
@@ -18,7 +18,6 @@ class DeploymentNavbar extends Component
|
||||
public Server $server;
|
||||
public bool $is_debug_enabled = false;
|
||||
protected $listeners = ['deploymentFinished'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Models\Application;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
@@ -25,7 +26,11 @@ class General extends Component
|
||||
public bool $labelsChanged = false;
|
||||
public bool $isConfigurationChanged = false;
|
||||
|
||||
public bool $is_static;
|
||||
public ?string $initialDockerComposeLocation = null;
|
||||
public ?string $initialDockerComposePrLocation = null;
|
||||
|
||||
public $parsedServices = [];
|
||||
public $parsedServiceDomains = [];
|
||||
|
||||
protected $listeners = [
|
||||
'resetDefaultLabels'
|
||||
@@ -50,6 +55,12 @@ class General extends Component
|
||||
'application.docker_registry_image_name' => 'nullable',
|
||||
'application.docker_registry_image_tag' => 'nullable',
|
||||
'application.dockerfile_location' => 'nullable',
|
||||
'application.docker_compose_location' => 'nullable',
|
||||
'application.docker_compose_pr_location' => 'nullable',
|
||||
'application.docker_compose' => 'nullable',
|
||||
'application.docker_compose_pr' => 'nullable',
|
||||
'application.docker_compose_raw' => 'nullable',
|
||||
'application.docker_compose_pr_raw' => 'nullable',
|
||||
'application.custom_labels' => 'nullable',
|
||||
'application.dockerfile_target_build' => 'nullable',
|
||||
'application.settings.is_static' => 'boolean|required',
|
||||
@@ -74,6 +85,12 @@ class General extends Component
|
||||
'application.docker_registry_image_name' => 'Docker registry image name',
|
||||
'application.docker_registry_image_tag' => 'Docker registry image tag',
|
||||
'application.dockerfile_location' => 'Dockerfile location',
|
||||
'application.docker_compose_location' => 'Docker compose location',
|
||||
'application.docker_compose_pr_location' => 'Docker compose location',
|
||||
'application.docker_compose' => 'Docker compose',
|
||||
'application.docker_compose_pr' => 'Docker compose',
|
||||
'application.docker_compose_raw' => 'Docker compose raw',
|
||||
'application.docker_compose_pr_raw' => 'Docker compose raw',
|
||||
'application.custom_labels' => 'Custom labels',
|
||||
'application.dockerfile_target_build' => 'Dockerfile target build',
|
||||
'application.settings.is_static' => 'Is static',
|
||||
@@ -81,6 +98,13 @@ class General extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$this->parsedServices = $this->application->parseCompose();
|
||||
} catch (\Throwable $e) {
|
||||
$this->emit('error', $e->getMessage());
|
||||
}
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
|
||||
$this->application->isConfigurationChanged(true);
|
||||
@@ -91,17 +115,54 @@ class General extends Component
|
||||
} else {
|
||||
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
|
||||
}
|
||||
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
|
||||
$this->checkLabelUpdates();
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
$this->application->settings->save();
|
||||
$this->emit('success', 'Settings saved.');
|
||||
$this->application->refresh();
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes) {
|
||||
$this->resetDefaultLabels(false);
|
||||
}
|
||||
}
|
||||
public function loadComposeFile($isInit = false)
|
||||
{
|
||||
try {
|
||||
if ($isInit && $this->application->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
|
||||
$this->emit('success', 'Docker compose file loaded.');
|
||||
} catch (\Throwable $e) {
|
||||
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
|
||||
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
|
||||
$this->application->save();
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function generateDomain(string $serviceName)
|
||||
{
|
||||
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
|
||||
if (!$domain) {
|
||||
$uuid = new Cuid2(7);
|
||||
$domain = generateFqdn($this->application->destination->server, $uuid);
|
||||
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
$this->application->save();
|
||||
$this->emit('success', 'Domain generated.');
|
||||
}
|
||||
return $domain;
|
||||
}
|
||||
public function updatedApplicationBuildPack()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
$this->application->settings->is_static = $this->is_static = false;
|
||||
$this->application->settings->is_static = false;
|
||||
$this->application->settings->save();
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->application->fqdn = null;
|
||||
$this->application->settings->save();
|
||||
}
|
||||
$this->submit();
|
||||
@@ -140,6 +201,9 @@ class General extends Component
|
||||
public function submit($showToaster = true)
|
||||
{
|
||||
try {
|
||||
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
|
||||
$this->loadComposeFile();
|
||||
}
|
||||
$this->validate();
|
||||
if ($this->ports_exposes !== $this->application->ports_exposes) {
|
||||
$this->resetDefaultLabels(false);
|
||||
@@ -172,6 +236,10 @@ class General extends Component
|
||||
$this->customLabels = str($this->customLabels)->replace(',', "\n");
|
||||
}
|
||||
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
$this->parsedServices = $this->application->parseCompose();
|
||||
}
|
||||
$this->application->save();
|
||||
$showToaster && $this->emit('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -39,8 +39,32 @@ class Heading extends Component
|
||||
$this->deploy(force_rebuild: true);
|
||||
}
|
||||
|
||||
public function deployNew()
|
||||
{
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->emit('error', 'Please load a Compose file first.');
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: false,
|
||||
is_new_deployment: true,
|
||||
);
|
||||
return redirect()->route('project.application.deployment', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
public function deploy(bool $force_rebuild = false)
|
||||
{
|
||||
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
|
||||
$this->emit('error', 'Please load a Compose file first.');
|
||||
return;
|
||||
}
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
@@ -68,7 +92,24 @@ class Heading extends Component
|
||||
$this->application->save();
|
||||
$this->application->refresh();
|
||||
}
|
||||
public function restart() {
|
||||
public function restartNew()
|
||||
{
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
is_new_deployment: true,
|
||||
);
|
||||
return redirect()->route('project.application.deployment', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $this->deploymentUuid,
|
||||
'environment_name' => $this->parameters['environment_name'],
|
||||
]);
|
||||
}
|
||||
public function restart()
|
||||
{
|
||||
$this->setDeploymentUuid();
|
||||
queue_application_deployment(
|
||||
application_id: $this->application->id,
|
||||
|
||||
@@ -72,10 +72,12 @@ class Previews extends Component
|
||||
public function stop(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$container_name = generateApplicationContainerName($this->application, $pull_request_id);
|
||||
|
||||
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
|
||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();
|
||||
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
|
||||
foreach ($containers as $container) {
|
||||
$name = str_replace('/', '', $container['Names']);
|
||||
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
|
||||
}
|
||||
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
|
||||
$this->application->refresh();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -22,79 +22,19 @@ class DockerCompose extends Component
|
||||
$this->query = request()->query();
|
||||
if (isDev()) {
|
||||
$this->dockerComposeRaw = 'services:
|
||||
ghost:
|
||||
image: ghost:5
|
||||
volumes:
|
||||
- ~/configs:/etc/configs/:ro
|
||||
- ./var/lib/ghost/content:/tmp/ghost2/content:ro
|
||||
- /var/lib/ghost/content:/tmp/ghost/content:rw
|
||||
- ghost-content-data:/var/lib/ghost/content
|
||||
- type: volume
|
||||
source: mydata
|
||||
target: /data
|
||||
- type: bind
|
||||
source: ./var/lib/ghost/data
|
||||
target: /data
|
||||
- type: bind
|
||||
source: /tmp
|
||||
target: /tmp
|
||||
labels:
|
||||
- "test.label=true"
|
||||
ports:
|
||||
- "3000"
|
||||
- "3000-3005"
|
||||
- "8000:8000"
|
||||
- "9090-9091:8080-8081"
|
||||
- "49100:22"
|
||||
- "127.0.0.1:8001:8001"
|
||||
- "127.0.0.1:5000-5010:5000-5010"
|
||||
- "127.0.0.1::5000"
|
||||
- "6060:6060/udp"
|
||||
- "12400-12500:1240"
|
||||
- target: 80
|
||||
published: 8080
|
||||
protocol: tcp
|
||||
mode: host
|
||||
networks:
|
||||
- some-network
|
||||
- other-network
|
||||
appsmith:
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
FROM nginx
|
||||
ARG GIT_COMMIT
|
||||
ARG GIT_BRANCH
|
||||
RUN echo "Hello World ${GIT_COMMIT} ${GIT_BRANCH}"
|
||||
args:
|
||||
- GIT_COMMIT=cdc3b19
|
||||
- GIT_BRANCH=${GIT_BRANCH}
|
||||
environment:
|
||||
- database__client=${DATABASE_CLIENT:-mysql}
|
||||
- database__connection__database=${MYSQL_DATABASE:-ghost}
|
||||
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
|
||||
- test=${TEST:?true}
|
||||
- url=$SERVICE_FQDN_GHOST
|
||||
- database__connection__user=$SERVICE_USER_MYSQL
|
||||
- database__connection__password=$SERVICE_PASSWORD_MYSQL
|
||||
depends_on:
|
||||
- mysql
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- ghost-mysql-data:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_USER=${SERVICE_USER_MYSQL}
|
||||
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
|
||||
- MYSQL_DATABASE=$MYSQL_DATABASE
|
||||
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
|
||||
- SESSION_SECRET
|
||||
minio:
|
||||
image: minio/minio
|
||||
environment:
|
||||
RACK_ENV: development
|
||||
A: $A
|
||||
SHOW: ${SHOW}
|
||||
SHOW1: ${SHOW2-show1}
|
||||
SHOW2: ${SHOW3:-show2}
|
||||
SHOW3: ${SHOW4?show3}
|
||||
SHOW4: ${SHOW5:?show4}
|
||||
SHOW5: ${SERVICE_USER_MINIO}
|
||||
SHOW6: ${SERVICE_PASSWORD_MINIO}
|
||||
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
|
||||
SHOW8: ${SERVICE_BASE64_64_MINIO}
|
||||
SHOW9: ${SERVICE_BASE64_128_MINIO}
|
||||
SHOW10: ${SERVICE_BASE64_MINIO}
|
||||
SHOW11:
|
||||
- APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED}
|
||||
';
|
||||
}
|
||||
}
|
||||
@@ -129,7 +69,7 @@ class DockerCompose extends Component
|
||||
|
||||
$service->parse(isNew: true);
|
||||
|
||||
return redirect()->route('project.service', [
|
||||
return redirect()->route('project.service.configuration', [
|
||||
'service_uuid' => $service->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
'project_uuid' => $project->uuid,
|
||||
|
||||
@@ -47,7 +47,7 @@ class Select extends Component
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
$this->loadServices();
|
||||
if ($this->search) $this->loadServices();
|
||||
return view('livewire.project.new.select');
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ class Select extends Component
|
||||
// }
|
||||
// }
|
||||
|
||||
public function loadServices(bool $force = false)
|
||||
public function loadServices()
|
||||
{
|
||||
try {
|
||||
if (count($this->allServices) > 0 && !$force) {
|
||||
if (count($this->allServices) > 0) {
|
||||
if (!$this->search) {
|
||||
$this->services = $this->allServices;
|
||||
return;
|
||||
|
||||
@@ -28,7 +28,12 @@ class Application extends Component
|
||||
}
|
||||
public function instantSaveAdvanced()
|
||||
{
|
||||
$this->submit();
|
||||
if (!$this->application->service->destination->server->isLogDrainEnabled()) {
|
||||
$this->application->is_log_drain_enabled = false;
|
||||
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
return;
|
||||
}
|
||||
$this->application->save();
|
||||
$this->emit('success', 'You need to restart the service for the changes to take effect.');
|
||||
}
|
||||
public function delete()
|
||||
@@ -36,7 +41,7 @@ class Application extends Component
|
||||
try {
|
||||
$this->application->delete();
|
||||
$this->emit('success', 'Application deleted successfully.');
|
||||
return redirect()->route('project.service', $this->parameters);
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -49,11 +54,6 @@ class Application extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
if (!$this->application->service->destination->server->isLogDrainEnabled()) {
|
||||
$this->application->is_log_drain_enabled = false;
|
||||
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
|
||||
return;
|
||||
}
|
||||
$this->application->save();
|
||||
updateCompose($this->application);
|
||||
$this->emit('success', 'Application saved successfully.');
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Project\Service;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ComposeModal extends Component
|
||||
{
|
||||
public ?string $raw = null;
|
||||
public ?string $actual = null;
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.service.compose-modal');
|
||||
}
|
||||
public function submit() {
|
||||
$this->emit('warning', "Saving new docker compose...");
|
||||
$this->emit('saveCompose', $this->raw);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class Index extends Component
|
||||
}
|
||||
public function checkStatus()
|
||||
{
|
||||
dispatch_sync(new ContainerStatusJob($this->service->server));
|
||||
dispatch(new ContainerStatusJob($this->service->server));
|
||||
$this->refreshStacks();
|
||||
}
|
||||
public function refreshStacks()
|
||||
|
||||
@@ -23,7 +23,7 @@ class StackForm extends Component
|
||||
foreach ($fields as $fieldKey => $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules');
|
||||
$rules = data_get($field, 'rules', 'nullable');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$this->fields[$key] = [
|
||||
"serviceName" => $serviceName,
|
||||
@@ -31,6 +31,7 @@ class StackForm extends Component
|
||||
"name" => $fieldKey,
|
||||
"value" => $value,
|
||||
"isPassword" => $isPassword,
|
||||
"rules" => $rules
|
||||
];
|
||||
$this->rules["fields.$key.value"] = $rules;
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
|
||||
110
app/Http/Livewire/Project/Shared/ExecuteContainerCommand.php
Normal file
110
app/Http/Livewire/Project/Shared/ExecuteContainerCommand.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Project\Shared;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
public string $command;
|
||||
public string $container;
|
||||
public $containers;
|
||||
public $parameters;
|
||||
public $resource;
|
||||
public string $type;
|
||||
public string $workDir = '';
|
||||
public Server $server;
|
||||
public $servers = [];
|
||||
|
||||
protected $rules = [
|
||||
'server' => 'required',
|
||||
'container' => 'required',
|
||||
'command' => 'required',
|
||||
'workDir' => 'nullable',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->server = $this->resource->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
}
|
||||
} else if (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->type = 'database';
|
||||
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMysql::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
$resource = StandaloneMariadb::where('uuid', $this->parameters['database_uuid'])->first();
|
||||
if (is_null($resource)) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$this->server = $this->resource->destination->server;
|
||||
$this->container = $this->resource->uuid;
|
||||
$this->containers->push($this->container);
|
||||
} else if (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->resource->applications()->get()->each(function ($application) {
|
||||
if (str(data_get($application, 'status'))->contains('running')) {
|
||||
$this->containers->push(data_get($application, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
}
|
||||
});
|
||||
$this->resource->databases()->get()->each(function ($database) {
|
||||
if (str(data_get($database, 'status'))->contains('running')) {
|
||||
$this->containers->push(data_get($database, 'name') . '-' . data_get($this->resource, 'uuid'));
|
||||
}
|
||||
});
|
||||
|
||||
$this->server = $this->resource->server;
|
||||
}
|
||||
if ($this->containers->count() > 0) {
|
||||
$this->container = $this->containers->first();
|
||||
}
|
||||
}
|
||||
|
||||
public function runCommand()
|
||||
{
|
||||
$this->validate();
|
||||
try {
|
||||
if (!empty($this->workDir)) {
|
||||
$exec = "docker exec -w {$this->workDir} {$this->container} {$this->command}";
|
||||
} else {
|
||||
$exec = "docker exec {$this->container} {$this->command}";
|
||||
}
|
||||
$activity = remote_process([$exec], $this->server, ignore_errors: true);
|
||||
$this->emit('newMonitorActivity', $activity->id);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.execute-container-command');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,16 @@
|
||||
|
||||
namespace App\Http\Livewire\Project\Shared;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -10,17 +19,44 @@ class GetLogs extends Component
|
||||
{
|
||||
public string $outputs = '';
|
||||
public string $errors = '';
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
|
||||
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
|
||||
public Server $server;
|
||||
public ?string $container = null;
|
||||
public ?bool $streamLogs = false;
|
||||
public ?bool $showTimeStamps = true;
|
||||
public int $numberOfLines = 100;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if ($this->resource->getMorphClass() === 'App\Models\Application') {
|
||||
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
|
||||
} else {
|
||||
if ($this->servicesubtype) {
|
||||
$this->showTimeStamps = $this->servicesubtype->is_include_timestamps;
|
||||
} else {
|
||||
$this->showTimeStamps = $this->resource->is_include_timestamps;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function doSomethingWithThisChunkOfOutput($output)
|
||||
{
|
||||
$this->outputs .= removeAnsiColors($output);
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->resource->getMorphClass() === 'App\Models\Application') {
|
||||
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->resource->settings->save();
|
||||
} else {
|
||||
if ($this->servicesubtype) {
|
||||
$this->servicesubtype->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->servicesubtype->save();
|
||||
} else {
|
||||
$this->resource->is_include_timestamps = $this->showTimeStamps;
|
||||
$this->resource->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
public function getLogs($refresh = false)
|
||||
{
|
||||
|
||||
@@ -17,13 +17,16 @@ class Logs extends Component
|
||||
public ?string $type = null;
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
|
||||
public Server $server;
|
||||
public ?string $container = null;
|
||||
public $container = [];
|
||||
public $containers;
|
||||
public $parameters;
|
||||
public $query;
|
||||
public $status;
|
||||
public $serviceSubType;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
@@ -33,7 +36,9 @@ class Logs extends Component
|
||||
$this->server = $this->resource->destination->server;
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
|
||||
if ($containers->count() > 0) {
|
||||
$this->container = data_get($containers[0], 'Names');
|
||||
$containers->each(function ($container) {
|
||||
$this->containers->push(str_replace('/', '', $container['Names']));
|
||||
});
|
||||
}
|
||||
} else if (data_get($this->parameters, 'database_uuid')) {
|
||||
$this->type = 'database';
|
||||
@@ -60,6 +65,11 @@ class Logs extends Component
|
||||
} else if (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$service_name = data_get($this->parameters, 'service_name');
|
||||
$this->serviceSubType = $this->resource->applications()->where('name', $service_name)->first();
|
||||
if (!$this->serviceSubType) {
|
||||
$this->serviceSubType = $this->resource->databases()->where('name', $service_name)->first();
|
||||
}
|
||||
$this->status = $this->resource->status;
|
||||
$this->server = $this->resource->server;
|
||||
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid;
|
||||
|
||||
@@ -17,14 +17,15 @@ class Form extends Component
|
||||
protected $listeners = ['serverRefresh'];
|
||||
|
||||
protected $rules = [
|
||||
'server.name' => 'required|min:6',
|
||||
'server.name' => 'required',
|
||||
'server.description' => 'nullable',
|
||||
'server.ip' => 'required',
|
||||
'server.user' => 'required',
|
||||
'server.port' => 'required',
|
||||
'server.settings.is_cloudflare_tunnel' => 'required',
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
'server.settings.is_reachable' => 'required',
|
||||
'server.settings.is_part_of_swarm' => 'required',
|
||||
'server.settings.is_swarm_manager' => 'required|boolean',
|
||||
// 'server.settings.is_swarm_worker' => 'required|boolean',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
@@ -34,14 +35,16 @@ class Form extends Component
|
||||
'server.user' => 'User',
|
||||
'server.port' => 'Port',
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
'server.settings.is_reachable' => 'is reachable',
|
||||
'server.settings.is_part_of_swarm' => 'is part of swarm'
|
||||
'server.settings.is_reachable' => 'Is reachable',
|
||||
'server.settings.is_swarm_manager' => 'Swarm Manager',
|
||||
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
||||
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
|
||||
$this->validateServer();
|
||||
}
|
||||
public function serverRefresh($install = true)
|
||||
{
|
||||
@@ -49,9 +52,14 @@ class Form extends Component
|
||||
}
|
||||
public function instantSave()
|
||||
{
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->validateServer();
|
||||
$this->server->settings->save();
|
||||
try {
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->validateServer(false);
|
||||
$this->server->settings->save();
|
||||
$this->emit('success', 'Server updated successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function installDocker()
|
||||
{
|
||||
@@ -100,6 +108,12 @@ class Form extends Component
|
||||
$install && $this->installDocker();
|
||||
return;
|
||||
}
|
||||
if ($this->server->isSwarm()) {
|
||||
$swarmInstalled = $this->server->validateDockerSwarm();
|
||||
if ($swarmInstalled) {
|
||||
$install && $this->emit('success', 'Docker Swarm is initiated.');
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
||||
@@ -19,6 +19,9 @@ class LogDrains extends Component
|
||||
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
|
||||
'server.settings.logdrain_axiom_api_key' => 'required|string',
|
||||
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_custom_config' => 'required|string',
|
||||
'server.settings.logdrain_custom_config_parser' => 'nullable',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
|
||||
@@ -29,6 +32,9 @@ class LogDrains extends Component
|
||||
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
|
||||
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
|
||||
'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
|
||||
'server.settings.is_logdrain_custom_enabled' => 'Custom log drain',
|
||||
'server.settings.logdrain_custom_config' => 'Custom log drain configuration',
|
||||
'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -84,6 +90,7 @@ class LogDrains extends Component
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
'is_logdrain_custom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'highlight') {
|
||||
$this->validate([
|
||||
@@ -93,6 +100,7 @@ class LogDrains extends Component
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
'is_logdrain_custom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'axiom') {
|
||||
$this->validate([
|
||||
@@ -103,6 +111,18 @@ class LogDrains extends Component
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
'is_logdrain_custom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'custom') {
|
||||
$this->validate([
|
||||
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
|
||||
'server.settings.logdrain_custom_config' => 'required|string',
|
||||
'server.settings.logdrain_custom_config_parser' => 'nullable',
|
||||
]);
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_newrelic_enabled' => false,
|
||||
'is_logdrain_highlight_enabled' => false,
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
]);
|
||||
}
|
||||
$this->server->settings->save();
|
||||
@@ -121,6 +141,10 @@ class LogDrains extends Component
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_axiom_enabled' => false,
|
||||
]);
|
||||
} else if ($type === 'custom') {
|
||||
$this->server->settings->update([
|
||||
'is_logdrain_custom_enabled' => false,
|
||||
]);
|
||||
}
|
||||
handleError($e, $this);
|
||||
return false;
|
||||
|
||||
@@ -21,7 +21,7 @@ class ByIp extends Component
|
||||
public string $ip;
|
||||
public string $user = 'root';
|
||||
public int $port = 22;
|
||||
public bool $is_part_of_swarm = false;
|
||||
public bool $is_swarm_manager = false;
|
||||
|
||||
protected $rules = [
|
||||
'name' => 'required|string',
|
||||
@@ -29,6 +29,7 @@ class ByIp extends Component
|
||||
'ip' => 'required',
|
||||
'user' => 'required|string',
|
||||
'port' => 'required|integer',
|
||||
'is_swarm_manager' => 'required|boolean',
|
||||
];
|
||||
protected $validationAttributes = [
|
||||
'name' => 'Name',
|
||||
@@ -36,6 +37,7 @@ class ByIp extends Component
|
||||
'ip' => 'IP Address/Domain',
|
||||
'user' => 'User',
|
||||
'port' => 'Port',
|
||||
'is_swarm_manager' => 'Swarm Manager',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -72,11 +74,11 @@ class ByIp extends Component
|
||||
'proxy' => [
|
||||
"type" => ProxyTypes::TRAEFIK_V2->value,
|
||||
"status" => ProxyStatus::EXITED->value,
|
||||
]
|
||||
|
||||
],
|
||||
]);
|
||||
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
|
||||
$server->settings->is_swarm_manager = $this->is_swarm_manager;
|
||||
$server->settings->save();
|
||||
$server->addInitialNetwork();
|
||||
return redirect()->route('server.show', $server->uuid);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -58,11 +58,25 @@ class Deploy extends Component
|
||||
|
||||
public function stop()
|
||||
{
|
||||
instant_remote_process([
|
||||
"docker rm -f coolify-proxy",
|
||||
], $this->server);
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
try {
|
||||
if ($this->server->isSwarm()) {
|
||||
instant_remote_process([
|
||||
"docker service rm coolify-proxy_traefik",
|
||||
], $this->server);
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
} else {
|
||||
instant_remote_process([
|
||||
"docker rm -f coolify-proxy",
|
||||
], $this->server);
|
||||
$this->server->proxy->status = 'exited';
|
||||
$this->server->save();
|
||||
$this->emit('proxyStatusUpdated');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,12 @@ class Status extends Component
|
||||
public int $numberOfPolls = 0;
|
||||
|
||||
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
|
||||
public function mount() {
|
||||
$this->checkProxy();
|
||||
}
|
||||
public function startProxyPolling()
|
||||
{
|
||||
$this->polling = true;
|
||||
$this->checkProxy();
|
||||
}
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ class Show extends Component
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.all');
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,17 @@ use Livewire\Component;
|
||||
|
||||
class Sponsorship extends Component
|
||||
{
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
return [
|
||||
"echo-private:custom.{$teamId},TestEvent" => 'testEvent',
|
||||
];
|
||||
}
|
||||
public function testEvent()
|
||||
{
|
||||
$this->emit('success', 'Realtime events configured!');
|
||||
}
|
||||
public function disable()
|
||||
{
|
||||
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
|
||||
|
||||
@@ -11,6 +11,9 @@ class PricingPlans extends Component
|
||||
public bool $isTrial = false;
|
||||
public function mount() {
|
||||
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
|
||||
if (config('constants.limits.trial_period') == 0) {
|
||||
$this->isTrial = false;
|
||||
}
|
||||
}
|
||||
public function subscribeStripe($type)
|
||||
{
|
||||
@@ -63,6 +66,7 @@ class PricingPlans extends Component
|
||||
];
|
||||
|
||||
if (!data_get($team,'subscription.stripe_trial_already_ended')) {
|
||||
if (config('constants.limits.trial_period') > 0) {
|
||||
$payload['subscription_data'] = [
|
||||
'trial_period_days' => config('constants.limits.trial_period'),
|
||||
'trial_settings' => [
|
||||
@@ -71,6 +75,7 @@ class PricingPlans extends Component
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
$payload['payment_method_collection'] = 'if_required';
|
||||
}
|
||||
$customer = currentTeam()->subscription?->stripe_customer_id ?? null;
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Traits\ExecuteRemoteCommandNew;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Throwable;
|
||||
|
||||
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
|
||||
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
public string $applicationDeploymentQueueId;
|
||||
|
||||
public function __construct(string $applicationDeploymentQueueId)
|
||||
{
|
||||
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
ray()->clearAll();
|
||||
ray('Deploying Docker Image');
|
||||
try {
|
||||
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
|
||||
$application = Application::find($applicationDeploymentQueue->application_id);
|
||||
|
||||
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
|
||||
$dockerImage = data_get($application, 'docker_registry_image_name');
|
||||
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
|
||||
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
|
||||
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
|
||||
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
|
||||
|
||||
$server = data_get($destination, 'server');
|
||||
$network = data_get($destination, 'network');
|
||||
|
||||
$containerName = generateApplicationContainerName($application, $pullRequestId);
|
||||
savePrivateKeyToFs($server);
|
||||
|
||||
ray("echo 'Starting deployment of {$productionImageName}.'");
|
||||
|
||||
$applicationDeploymentQueue->update([
|
||||
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
|
||||
]);
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: prepareHelperContainer($server, $network, $deploymentUuid)
|
||||
);
|
||||
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: generateComposeFile(
|
||||
deploymentUuid: $deploymentUuid,
|
||||
server: $server,
|
||||
network: $network,
|
||||
application: $application,
|
||||
containerName: $containerName,
|
||||
imageName: $productionImageName,
|
||||
pullRequestId: $pullRequestId
|
||||
)
|
||||
);
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->executeRemoteCommand(
|
||||
server: $server,
|
||||
logModel: $applicationDeploymentQueue,
|
||||
commands: [
|
||||
"echo 'Oops something is not okay, are you okay? 😢'",
|
||||
"echo '{$e->getMessage()}'",
|
||||
"echo -n 'Deployment failed. Removing the new version of your application.'",
|
||||
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
|
||||
]
|
||||
);
|
||||
// $this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
// private function next(string $status)
|
||||
// {
|
||||
// // If the deployment is cancelled by the user, don't update the status
|
||||
// if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
// $this->application_deployment_queue->update([
|
||||
// 'status' => $status,
|
||||
// ]);
|
||||
// }
|
||||
// queue_next_deployment($this->application);
|
||||
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
// }
|
||||
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
|
||||
class ApplicationDeploySimpleDockerfileJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
|
||||
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
public string $applicationDeploymentQueueId;
|
||||
|
||||
public function __construct(string $applicationDeploymentQueueId)
|
||||
{
|
||||
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
|
||||
}
|
||||
public function handle() {
|
||||
ray('Deploying Simple Dockerfile');
|
||||
}
|
||||
}
|
||||
@@ -73,9 +73,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private $docker_compose;
|
||||
private $docker_compose_base64;
|
||||
private string $dockerfile_location = '/Dockerfile';
|
||||
private string $docker_compose_location = '/docker-compose.yml';
|
||||
private ?string $addHosts = null;
|
||||
private ?string $buildTarget = null;
|
||||
private $log_model;
|
||||
private Collection $saved_outputs;
|
||||
private ?string $full_healthcheck_url = null;
|
||||
|
||||
@@ -92,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
public $tries = 1;
|
||||
public function __construct(int $application_deployment_queue_id)
|
||||
{
|
||||
// ray()->clearScreen();
|
||||
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
|
||||
$this->log_model = $this->application_deployment_queue;
|
||||
$this->application = Application::find($this->application_deployment_queue->application_id);
|
||||
$this->build_pack = data_get($this->application, 'build_pack');
|
||||
|
||||
@@ -114,7 +112,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
|
||||
$this->server = $this->mainServer = $this->destination->server;
|
||||
$this->serverUser = $this->server->user;
|
||||
$this->basedir = "/artifacts/{$this->deployment_uuid}";
|
||||
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
|
||||
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
|
||||
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
|
||||
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
|
||||
@@ -158,40 +156,35 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
// Generate custom host<->ip mapping
|
||||
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
$this->addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Check custom port
|
||||
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
|
||||
if (count($matches) === 1) {
|
||||
$this->customPort = $matches[0];
|
||||
$gitHost = str($this->application->git_repository)->before(':');
|
||||
$gitRepo = str($this->application->git_repository)->after('/');
|
||||
$this->customRepository = "$gitHost:$gitRepo";
|
||||
} else {
|
||||
$this->customRepository = $this->application->git_repository;
|
||||
}
|
||||
['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
|
||||
|
||||
try {
|
||||
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
|
||||
$this->just_restart();
|
||||
@@ -203,6 +196,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
return;
|
||||
} else if ($this->application->dockerfile) {
|
||||
$this->deploy_simple_dockerfile();
|
||||
} else if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->deploy_docker_compose_buildpack();
|
||||
} else if ($this->application->build_pack === 'dockerimage') {
|
||||
$this->deploy_dockerimage_buildpack();
|
||||
} else if ($this->application->build_pack === 'dockerfile') {
|
||||
@@ -219,8 +214,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
dispatch(new ContainerStatusJob($this->server));
|
||||
}
|
||||
if ($this->application->docker_registry_image_name) {
|
||||
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
|
||||
$this->push_to_docker_registry();
|
||||
if ($this->server->isSwarm()) {
|
||||
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
|
||||
],
|
||||
[
|
||||
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->application->isConfigurationChanged(true);
|
||||
@@ -297,42 +303,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
ray($e);
|
||||
}
|
||||
}
|
||||
// private function deploy_docker_compose()
|
||||
// {
|
||||
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
|
||||
// $this->execute_remote_command(
|
||||
// [
|
||||
// "echo 'Starting deployment of {$this->application->name}.'"
|
||||
// ],
|
||||
// );
|
||||
// $this->prepare_builder_image();
|
||||
// $this->execute_remote_command(
|
||||
// [
|
||||
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
|
||||
// ],
|
||||
// );
|
||||
// $this->build_image_name = Str::lower("{$this->customRepository}:build");
|
||||
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
|
||||
// $this->save_environment_variables();
|
||||
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
|
||||
// ray($containers);
|
||||
// if ($containers->count() > 0) {
|
||||
// foreach ($containers as $container) {
|
||||
// $containerName = data_get($container, 'Names');
|
||||
// if ($containerName) {
|
||||
// instant_remote_process(
|
||||
// ["docker rm -f {$containerName}"],
|
||||
// $this->application->destination->server
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// $this->execute_remote_command(
|
||||
// ["echo -n 'Starting services (could take a while)...'"],
|
||||
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
|
||||
// );
|
||||
// }
|
||||
private function generate_image_names()
|
||||
{
|
||||
if ($this->application->dockerfile) {
|
||||
@@ -377,6 +347,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->generate_image_names();
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
|
||||
$this->create_workdir();
|
||||
$this->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
return;
|
||||
@@ -397,19 +368,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
]);
|
||||
}
|
||||
}
|
||||
// private function save_environment_variables()
|
||||
// {
|
||||
// $envs = collect([]);
|
||||
// foreach ($this->application->environment_variables as $env) {
|
||||
// $envs->push($env->key . '=' . $env->value);
|
||||
// }
|
||||
// $envs_base64 = base64_encode($envs->implode("\n"));
|
||||
// $this->execute_remote_command(
|
||||
// [
|
||||
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
private function save_environment_variables()
|
||||
{
|
||||
$envs = collect([]);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
foreach ($this->application->environment_variables_preview as $env) {
|
||||
$envs->push($env->key . '=' . $env->value);
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->environment_variables as $env) {
|
||||
$envs->push($env->key . '=' . $env->value);
|
||||
}
|
||||
}
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function deploy_simple_dockerfile()
|
||||
{
|
||||
$dockerfile_base64 = base64_encode($this->application->dockerfile);
|
||||
@@ -447,7 +425,67 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->generate_compose_file();
|
||||
$this->rolling_update();
|
||||
}
|
||||
private function deploy_docker_compose_buildpack()
|
||||
{
|
||||
if (data_get($this->application, 'docker_compose_location')) {
|
||||
$this->docker_compose_location = $this->application->docker_compose_location;
|
||||
}
|
||||
if ($this->pull_request_id === 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
|
||||
}
|
||||
$this->server->executeRemoteCommand(
|
||||
commands: $this->application->prepareHelperImage($this->deployment_uuid),
|
||||
loggingModel: $this->application_deployment_queue
|
||||
);
|
||||
$this->check_git_if_build_needed();
|
||||
$this->clone_repository();
|
||||
$this->generate_image_names();
|
||||
$this->cleanup_git();
|
||||
$this->application->loadComposeFile(isInit: false);
|
||||
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
|
||||
$yaml = Yaml::dump($composeFile->toArray(), 10);
|
||||
$this->docker_compose_base64 = base64_encode($yaml);
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true
|
||||
]);
|
||||
$this->save_environment_variables();
|
||||
$this->stop_running_container(force: true);
|
||||
|
||||
$networkId = $this->application->uuid;
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
|
||||
}
|
||||
if ($this->server->isSwarm()) {
|
||||
// TODO
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
"docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true
|
||||
], [
|
||||
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
|
||||
]);
|
||||
}
|
||||
if (isset($this->docker_compose_base64)) {
|
||||
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
|
||||
$composeFileName = "$this->configuration_dir/docker-compose.yml";
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"mkdir -p $this->configuration_dir"
|
||||
],
|
||||
[
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
|
||||
],
|
||||
[
|
||||
"echo '{$readme}' > $this->configuration_dir/README.md",
|
||||
]
|
||||
);
|
||||
}
|
||||
$this->start_by_compose_file();
|
||||
}
|
||||
private function deploy_dockerfile_buildpack()
|
||||
{
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
@@ -472,7 +510,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// $this->push_to_docker_registry();
|
||||
// $this->deploy_to_additional_destinations();
|
||||
// } else {
|
||||
$this->rolling_update();
|
||||
$this->rolling_update();
|
||||
// }
|
||||
}
|
||||
private function deploy_nixpacks_buildpack()
|
||||
@@ -489,6 +527,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if (!$this->force_rebuild) {
|
||||
$this->check_image_locally_or_remotely();
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
|
||||
$this->create_workdir();
|
||||
$this->execute_remote_command([
|
||||
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
|
||||
]);
|
||||
@@ -531,74 +570,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
private function rolling_update()
|
||||
{
|
||||
if (count($this->application->ports_mappings_array) > 0) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
|
||||
);
|
||||
$this->stop_running_container(force: true);
|
||||
$this->start_by_compose_file();
|
||||
if ($this->server->isSwarm()) {
|
||||
// Skip this.
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
["echo -n 'Rolling update started.'"],
|
||||
);
|
||||
$this->start_by_compose_file();
|
||||
$this->health_check();
|
||||
$this->stop_running_container();
|
||||
if (count($this->application->ports_mappings_array) > 0) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
|
||||
);
|
||||
$this->stop_running_container(force: true);
|
||||
$this->start_by_compose_file();
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
["echo -n 'Rolling update started.'"],
|
||||
);
|
||||
$this->start_by_compose_file();
|
||||
$this->health_check();
|
||||
$this->stop_running_container();
|
||||
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
private function health_check()
|
||||
{
|
||||
if ($this->application->isHealthcheckDisabled()) {
|
||||
$this->newVersionIsHealthy = true;
|
||||
return;
|
||||
}
|
||||
// ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Waiting for healthcheck to pass on the new container.'"
|
||||
]
|
||||
);
|
||||
if ($this->full_healthcheck_url) {
|
||||
if ($this->server->isSwarm()) {
|
||||
// Implement healthcheck for swarm
|
||||
} else {
|
||||
if ($this->application->isHealthcheckDisabled()) {
|
||||
$this->newVersionIsHealthy = true;
|
||||
return;
|
||||
}
|
||||
// ray('New container name: ', $this->container_name);
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
|
||||
"echo 'Waiting for healthcheck to pass on the new container.'"
|
||||
]
|
||||
);
|
||||
}
|
||||
while ($counter < $this->application->health_check_retries) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
|
||||
"hidden" => true,
|
||||
"save" => "health_check"
|
||||
],
|
||||
|
||||
);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
|
||||
],
|
||||
);
|
||||
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
|
||||
$this->newVersionIsHealthy = true;
|
||||
$this->application->update(['status' => 'running']);
|
||||
if ($this->full_healthcheck_url) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'New container is healthy.'"
|
||||
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
|
||||
]
|
||||
);
|
||||
}
|
||||
while ($counter < $this->application->health_check_retries) {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
|
||||
"hidden" => true,
|
||||
"save" => "health_check"
|
||||
],
|
||||
|
||||
);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
|
||||
],
|
||||
);
|
||||
break;
|
||||
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
|
||||
$this->newVersionIsHealthy = true;
|
||||
$this->application->update(['status' => 'running']);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'New container is healthy.'"
|
||||
],
|
||||
);
|
||||
break;
|
||||
}
|
||||
$counter++;
|
||||
sleep($this->application->health_check_interval);
|
||||
}
|
||||
$counter++;
|
||||
sleep($this->application->health_check_interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -618,8 +666,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
$this->generate_compose_file();
|
||||
// Needs separate preview variables
|
||||
// $this->generate_build_env_variables();
|
||||
// $this->add_build_env_variables_to_dockerfile();
|
||||
$this->generate_build_env_variables();
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
$this->build_image();
|
||||
$this->stop_running_container();
|
||||
$this->execute_remote_command(
|
||||
@@ -627,7 +675,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
|
||||
private function create_workdir()
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
|
||||
],
|
||||
);
|
||||
}
|
||||
private function prepare_builder_image()
|
||||
{
|
||||
$helperImage = config('coolify.helper_image');
|
||||
@@ -636,9 +691,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
|
||||
|
||||
if ($this->dockerConfigFileExists === 'OK') {
|
||||
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
|
||||
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
|
||||
} else {
|
||||
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
|
||||
$runCommand = "docker run -d --network {$this->destination->network} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
@@ -651,6 +706,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
[
|
||||
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
|
||||
],
|
||||
|
||||
);
|
||||
}
|
||||
private function deploy_to_additional_destinations()
|
||||
@@ -725,13 +781,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private function clone_repository()
|
||||
{
|
||||
$importCommands = $this->generate_git_import_commands();
|
||||
$this->application_deployment_queue->addLogEntry("\n----------------------------------------");
|
||||
$this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}.");
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '\n----------------------------------------'",
|
||||
],
|
||||
[
|
||||
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
|
||||
],
|
||||
[
|
||||
$importCommands, "hidden" => true
|
||||
]
|
||||
@@ -740,90 +795,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
private function generate_git_import_commands()
|
||||
{
|
||||
$this->branch = $this->application->git_branch;
|
||||
$commands = collect([]);
|
||||
$git_clone_command = "git clone -q -b {$this->application->git_branch}";
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$pr_branch_name = "pr-{$this->pull_request_id}-coolify";
|
||||
}
|
||||
|
||||
if ($this->application->deploymentType() === 'source') {
|
||||
$source_html_url = data_get($this->application, 'source.html_url');
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
|
||||
$source_html_url_host = $url['host'];
|
||||
$source_html_url_scheme = $url['scheme'];
|
||||
|
||||
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($this->source->is_public) {
|
||||
$this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
|
||||
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command);
|
||||
|
||||
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
$github_access_token = generate_github_installation_token($this->source);
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
|
||||
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
|
||||
}
|
||||
return $commands->implode(' && ');
|
||||
}
|
||||
}
|
||||
if ($this->application->deploymentType() === 'deploy_key') {
|
||||
$this->fullRepoUrl = $this->customRepository;
|
||||
$private_key = data_get($this->application, 'private_key.private_key');
|
||||
if (is_null($private_key)) {
|
||||
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
|
||||
}
|
||||
$private_key = base64_encode($private_key);
|
||||
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command_base);
|
||||
$commands = collect([
|
||||
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
|
||||
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
|
||||
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
|
||||
]);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
ray($this->git_type);
|
||||
if ($this->git_type === 'gitlab') {
|
||||
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
|
||||
}
|
||||
if ($this->git_type === 'github') {
|
||||
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
|
||||
}
|
||||
}
|
||||
|
||||
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
|
||||
return $commands->implode(' && ');
|
||||
}
|
||||
if ($this->application->deploymentType() === 'other') {
|
||||
$this->fullRepoUrl = $this->customRepository;
|
||||
$git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command);
|
||||
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
|
||||
return $commands->implode(' && ');
|
||||
}
|
||||
['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
|
||||
return $commands;
|
||||
}
|
||||
|
||||
private function set_git_import_settings($git_clone_command)
|
||||
{
|
||||
if ($this->application->git_commit_sha !== 'HEAD') {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
|
||||
}
|
||||
if ($this->application->settings->is_git_submodules_enabled) {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
|
||||
}
|
||||
if ($this->application->settings->is_git_lfs_enabled) {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
|
||||
}
|
||||
return $git_clone_command;
|
||||
return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
|
||||
}
|
||||
|
||||
private function cleanup_git()
|
||||
@@ -849,7 +827,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private function nixpacks_build_cmd()
|
||||
{
|
||||
$this->generate_env_variables();
|
||||
$nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
|
||||
$cacheKey = $this->application->uuid;
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}";
|
||||
}
|
||||
$nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
|
||||
if ($this->application->build_command) {
|
||||
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
|
||||
}
|
||||
@@ -899,21 +881,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$labels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
|
||||
// $newHostLabel = $newLabels->filter(function ($label) {
|
||||
// return str($label)->contains('Host');
|
||||
// });
|
||||
// $labels = $labels->reject(function ($label) {
|
||||
// return str($label)->contains('Host');
|
||||
// });
|
||||
// ray($labels,$newLabels);
|
||||
// $labels = $labels->map(function ($label) {
|
||||
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
|
||||
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
|
||||
// $newLabel = preg_replace($pattern, $replacement, $label);
|
||||
// return $newLabel;
|
||||
// });
|
||||
// $labels = $labels->merge($newHostLabel);
|
||||
}
|
||||
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
|
||||
$docker_compose = [
|
||||
@@ -924,7 +891,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
'container_name' => $this->container_name,
|
||||
'restart' => RESTART_MODE,
|
||||
'environment' => $environment_variables,
|
||||
'labels' => $labels,
|
||||
'expose' => $ports,
|
||||
'networks' => [
|
||||
$this->destination->network,
|
||||
@@ -956,6 +922,48 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($this->server->isSwarm()) {
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.restart');
|
||||
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.cpus');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset');
|
||||
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
|
||||
|
||||
$docker_compose['services'][$this->container_name]['deploy'] = [
|
||||
'placement' => [
|
||||
'constraints' => [
|
||||
'node.role == worker'
|
||||
]
|
||||
],
|
||||
'mode' => 'replicated',
|
||||
'replicas' => 1,
|
||||
'update_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'rollback_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'labels' => $labels,
|
||||
'resources' => [
|
||||
'limits' => [
|
||||
'cpus' => $this->application->limits_cpus,
|
||||
'memory' => $this->application->limits_memory,
|
||||
],
|
||||
'reservations' => [
|
||||
'cpus' => $this->application->limits_cpus,
|
||||
'memory' => $this->application->limits_memory,
|
||||
]
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$docker_compose['services'][$this->container_name]['labels'] = $labels;
|
||||
}
|
||||
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$this->container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
@@ -967,7 +975,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
];
|
||||
}
|
||||
if ($this->application->settings->is_gpu_enabled) {
|
||||
ray('asd');
|
||||
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
|
||||
[
|
||||
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
|
||||
@@ -1004,6 +1011,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
|
||||
// ];
|
||||
// }
|
||||
|
||||
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
|
||||
|
||||
data_forget($docker_compose, 'services.' . $this->container_name);
|
||||
|
||||
$this->docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$this->docker_compose_base64 = base64_encode($this->docker_compose);
|
||||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
|
||||
@@ -1082,6 +1094,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} else {
|
||||
$health_check_port = $this->application->health_check_port;
|
||||
}
|
||||
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
|
||||
$health_check_port = 80;
|
||||
}
|
||||
if ($this->application->health_check_path) {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
|
||||
$generated_healthchecks_commands = [
|
||||
@@ -1206,14 +1221,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
private function stop_running_container(bool $force = false)
|
||||
{
|
||||
$this->execute_remote_command(["echo -n 'Removing old container.'"]);
|
||||
$this->application_deployment_queue->addLogEntry("Removing old containers.");
|
||||
if ($this->newVersionIsHealthy || $force) {
|
||||
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$containers = $containers->filter(function ($container) {
|
||||
return data_get($container, 'Names') === $this->container_name;
|
||||
});
|
||||
} else {
|
||||
if ($this->pull_request_id === 0) {
|
||||
$containers = $containers->filter(function ($container) {
|
||||
return data_get($container, 'Names') !== $this->container_name;
|
||||
});
|
||||
@@ -1224,14 +1235,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
});
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo 'Rolling update completed.'"
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'New container is not healthy, rolling back to the old container.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
|
||||
);
|
||||
}
|
||||
@@ -1240,16 +1246,23 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
private function start_by_compose_file()
|
||||
{
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Pulling latest images from the registry.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
if ($this->docker_compose_location) {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
|
||||
);
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("New container started.");
|
||||
}
|
||||
|
||||
private function generate_build_env_variables()
|
||||
@@ -1274,9 +1287,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
|
||||
]);
|
||||
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
|
||||
|
||||
foreach ($this->application->build_environment_variables as $env) {
|
||||
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->build_environment_variables as $env) {
|
||||
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->build_environment_variables_preview as $env) {
|
||||
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
|
||||
}
|
||||
}
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
@@ -1307,9 +1325,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->execute_remote_command(
|
||||
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
|
||||
["echo '{$exception->getMessage()}'", 'type' => 'err'],
|
||||
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
|
||||
);
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
|
||||
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
|
||||
);
|
||||
}
|
||||
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
}
|
||||
|
||||
180
app/Jobs/ApplicationDeploymentNewJob.php
Normal file
180
app/Jobs/ApplicationDeploymentNewJob.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Server;
|
||||
use App\Traits\ExecuteRemoteCommand;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class ApplicationDeploymentNewJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
|
||||
|
||||
public $timeout = 3600;
|
||||
public $tries = 1;
|
||||
|
||||
public static int $batch_counter = 0;
|
||||
public Server $mainServer;
|
||||
public $servers;
|
||||
public string $basedir;
|
||||
public string $workdir;
|
||||
|
||||
public string $deploymentUuid;
|
||||
public int $pullRequestId = 0;
|
||||
|
||||
// Git related
|
||||
public string $gitImportCommands;
|
||||
public ?string $gitType = null;
|
||||
public string $gitRepository;
|
||||
public string $gitBranch;
|
||||
public int $gitPort;
|
||||
public string $gitFullRepoUrl;
|
||||
|
||||
public function __construct(public ApplicationDeploymentQueue $deployment, public Application $application)
|
||||
{
|
||||
$this->mainServer = data_get($this->application, 'destination.server');
|
||||
$this->deploymentUuid = data_get($this->deployment, 'deployment_uuid');
|
||||
$this->pullRequestId = data_get($this->deployment, 'pull_request_id', 0);
|
||||
$this->gitType = data_get($this->deployment, 'git_type');
|
||||
|
||||
$this->basedir = $this->application->generateBaseDir($this->deploymentUuid);
|
||||
$this->workdir = $this->basedir . rtrim($this->application->base_directory, '/');
|
||||
}
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
ray()->clearAll();
|
||||
$this->deployment->setStatus(ApplicationDeploymentStatus::IN_PROGRESS->value);
|
||||
|
||||
$hostIpMappings = $this->mainServer->getHostIPMappings($this->application->destination->network);
|
||||
if ($this->application->dockerfile_target_build) {
|
||||
$buildTarget = " --target {$this->application->dockerfile_target_build} ";
|
||||
}
|
||||
|
||||
// Get the git repository and port (custom port or default port)
|
||||
[
|
||||
'repository' => $this->gitRepository,
|
||||
'port' => $this->gitPort
|
||||
] = $this->application->customRepository();
|
||||
|
||||
// Get the git branch and git import commands
|
||||
[
|
||||
'commands' => $this->gitImportCommands,
|
||||
'branch' => $this->gitBranch,
|
||||
'fullRepoUrl' => $this->gitFullRepoUrl
|
||||
] = $this->application->generateGitImportCommands($this->deploymentUuid, $this->pullRequestId, $this->gitType);
|
||||
|
||||
$this->servers = $this->application->servers();
|
||||
|
||||
if ($this->deployment->restart_only) {
|
||||
if ($this->application->build_pack === 'dockerimage') {
|
||||
throw new \Exception('Restart only is not supported for docker image based deployments');
|
||||
}
|
||||
$this->deployment->addLogEntry("Starting deployment of {$this->application->name}.");
|
||||
$this->servers->each(function ($server) {
|
||||
$this->deployment->addLogEntry("Restarting {$this->application->name} on {$server->name}.");
|
||||
$this->restartOnly($server);
|
||||
});
|
||||
}
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
} catch (Throwable $exception) {
|
||||
$this->fail($exception);
|
||||
} finally {
|
||||
$this->servers->each(function ($server) {
|
||||
$this->deployment->addLogEntry("Cleaning up temporary containers on {$server->name}.");
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
"command" => "docker rm -f {$this->deploymentUuid}",
|
||||
"hidden" => true,
|
||||
"ignoreErrors" => true,
|
||||
]),
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
public function restartOnly(Server $server)
|
||||
{
|
||||
$server->executeRemoteCommand(
|
||||
commands: $this->application->prepareHelperImage($this->deploymentUuid),
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
|
||||
$privateKey = data_get($this->application, 'private_key.private_key', null);
|
||||
$gitLsRemoteCommand = collect([]);
|
||||
if ($privateKey) {
|
||||
$privateKey = base64_decode($privateKey);
|
||||
$gitLsRemoteCommand
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "mkdir -p /root/.ssh")
|
||||
])
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "echo '{$privateKey}' | base64 -d > /root/.ssh/id_rsa")
|
||||
])
|
||||
->push([
|
||||
"command" => executeInDocker($this->deploymentUuid, "chmod 600 /root/.ssh/id_rsa")
|
||||
])
|
||||
->push([
|
||||
"name" => "git_commit_sha",
|
||||
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$gitLsRemoteCommand->push([
|
||||
"name" => "git_commit_sha",
|
||||
"command" => executeInDocker($this->deploymentUuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->gitPort} -o Port={$this->gitPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->gitFullRepoUrl} {$this->gitBranch}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
}
|
||||
$this->deployment->addLogEntry("Checking if there is any new commit on {$this->gitBranch} branch.");
|
||||
|
||||
$server->executeRemoteCommand(
|
||||
commands: $gitLsRemoteCommand,
|
||||
loggingModel: $this->deployment
|
||||
);
|
||||
$commit = str($this->deployment->getOutput('git_commit_sha'))->before("\t");
|
||||
|
||||
[
|
||||
'productionImageName' => $productionImageName
|
||||
] = $this->application->generateImageNames($commit, $this->pullRequestId);
|
||||
|
||||
$this->deployment->addLogEntry("Checking if the image {$productionImageName} already exists.");
|
||||
$server->checkIfDockerImageExists($productionImageName, $this->deployment);
|
||||
|
||||
if (str($this->deployment->getOutput('local_image_found'))->isNotEmpty()) {
|
||||
$this->deployment->addLogEntry("Image {$productionImageName} already exists. Skipping the build.");
|
||||
|
||||
$server->createWorkDirForDeployment($this->workdir, $this->deployment);
|
||||
|
||||
$this->application->generateDockerComposeFile($server, $this->deployment, $this->workdir);
|
||||
$this->application->rollingUpdateApplication($server, $this->deployment, $this->workdir);
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
|
||||
}
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
ray($exception);
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
}
|
||||
private function next(string $status)
|
||||
{
|
||||
// If the deployment is cancelled by the user, don't update the status
|
||||
if ($this->deployment->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
$this->deployment->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
queue_next_deployment($this->application, isNew: true);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if (!$this->server->isServerReady()) {
|
||||
return;
|
||||
};
|
||||
$containers = instant_remote_process(["docker container ls -q"], $this->server);
|
||||
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
|
||||
if (!$containers) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Models\ApplicationPreview;
|
||||
@@ -21,10 +22,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
$this->handle();
|
||||
}
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
|
||||
@@ -35,34 +32,75 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
return $this->server->id;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
public function __construct(public Server $server)
|
||||
{
|
||||
if (isDev()) $this->handle();
|
||||
}
|
||||
|
||||
|
||||
public function handle()
|
||||
{
|
||||
// ray("checking container statuses for {$this->server->id}");
|
||||
try {
|
||||
if (!$this->server->isServerReady()) {
|
||||
return;
|
||||
};
|
||||
$containers = instant_remote_process(["docker container ls -q"], $this->server);
|
||||
if (!$containers) {
|
||||
if ($this->server->isSwarm()) {
|
||||
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
|
||||
} else {
|
||||
// Precheck for containers
|
||||
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
|
||||
if (!$containers) {
|
||||
return;
|
||||
}
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
|
||||
$containerReplicase = null;
|
||||
}
|
||||
if (is_null($containers)) {
|
||||
return;
|
||||
}
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
if ($containerReplicase) {
|
||||
$containerReplicase = format_docker_command_output_to_json($containerReplicase);
|
||||
foreach ($containerReplicase 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
$applications = $this->server->applications();
|
||||
$databases = $this->server->databases();
|
||||
$services = $this->server->services()->get();
|
||||
$previews = $this->server->previews();
|
||||
|
||||
$foundApplications = [];
|
||||
$foundApplicationPreviews = [];
|
||||
$foundDatabases = [];
|
||||
$foundServices = [];
|
||||
|
||||
foreach ($containers as $container) {
|
||||
if ($this->server->isSwarm()) {
|
||||
$labels = data_get($container, 'Spec.Labels');
|
||||
$uuid = data_get($labels, 'coolify.name');
|
||||
} else {
|
||||
$labels = data_get($container, 'Config.Labels');
|
||||
}
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$labels = data_get($container, 'Config.Labels');
|
||||
$labels = Arr::undot(format_docker_labels_to_json($labels));
|
||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||
if ($applicationId) {
|
||||
@@ -98,11 +136,25 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($uuid) {
|
||||
$database = $databases->where('uuid', $uuid)->first();
|
||||
if ($database) {
|
||||
$isPublic = data_get($database, 'is_public');
|
||||
$foundDatabases[] = $database->id;
|
||||
$statusFromDb = $database->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$database->update(['status' => $containerStatus]);
|
||||
}
|
||||
if ($isPublic) {
|
||||
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
|
||||
} else {
|
||||
return data_get($value, 'Name') === "/$uuid-proxy";
|
||||
}
|
||||
})->first();
|
||||
if (!$foundTcpProxy) {
|
||||
StartDatabaseProxy::run($database);
|
||||
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Notify user that this container should not be there.
|
||||
}
|
||||
@@ -167,7 +219,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
}
|
||||
|
||||
@@ -194,7 +246,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
|
||||
foreach ($notRunningApplicationPreviews as $previewId) {
|
||||
@@ -219,7 +271,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
|
||||
foreach ($notRunningDatabases as $database) {
|
||||
@@ -243,22 +295,24 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $containers->filter(function ($value, $key) {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (!$foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
} else {
|
||||
ray('Proxy could not be started.');
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
@@ -272,7 +326,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
|
||||
ray($e->getMessage());
|
||||
handleError($e);
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Notifications\Server\HighDiskUsage;
|
||||
use Exception;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
@@ -13,6 +11,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
|
||||
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
{
|
||||
@@ -35,7 +34,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
});
|
||||
if ($isInprogress) {
|
||||
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
|
||||
throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
|
||||
}
|
||||
if (!$this->server->isFunctional()) {
|
||||
return;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,12 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Application extends BaseModel
|
||||
{
|
||||
@@ -45,7 +49,76 @@ class Application extends BaseModel
|
||||
$application->environment_variables_preview()->delete();
|
||||
});
|
||||
}
|
||||
// Build packs / deployment types
|
||||
|
||||
|
||||
public function servers(): Collection
|
||||
{
|
||||
$mainServer = data_get($this, 'destination.server');
|
||||
$additionalDestinations = data_get($this, 'additional_destinations', null);
|
||||
$additionalServers = collect([]);
|
||||
if ($this->isMultipleServerDeployment()) {
|
||||
ray('asd');
|
||||
if (str($additionalDestinations)->isNotEmpty()) {
|
||||
$additionalDestinations = str($additionalDestinations)->explode(',');
|
||||
foreach ($additionalDestinations as $destinationId) {
|
||||
$destination = StandaloneDocker::find($destinationId)->whereNot('id', $mainServer->id)->first();
|
||||
$server = data_get($destination, 'server');
|
||||
$additionalServers->push($server);
|
||||
}
|
||||
}
|
||||
}
|
||||
return collect([$mainServer])->merge($additionalServers);
|
||||
}
|
||||
|
||||
public function generateImageNames(string $commit, int $pullRequestId)
|
||||
{
|
||||
if ($this->dockerfile) {
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:latest");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:latest");
|
||||
}
|
||||
} else if ($this->build_pack === 'dockerimage') {
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$this->docker_registry_image_tag}");
|
||||
} else if ($pullRequestId === 0) {
|
||||
$dockerImageTag = str($commit)->substr(0, 128);
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}-build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:{$dockerImageTag}");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:{$dockerImageTag}-build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:{$dockerImageTag}");
|
||||
}
|
||||
} else if ($pullRequestId !== 0) {
|
||||
if ($this->docker_registry_image_name) {
|
||||
$buildImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}-build");
|
||||
$productionImageName = Str::lower("{$this->docker_registry_image_name}:pr-{$pullRequestId}");
|
||||
} else {
|
||||
$buildImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}-build");
|
||||
$productionImageName = Str::lower("{$this->uuid}:pr-{$pullRequestId}");
|
||||
}
|
||||
}
|
||||
return [
|
||||
'buildImageName' => $buildImageName,
|
||||
'productionImageName' => $productionImageName,
|
||||
];
|
||||
}
|
||||
// End of build packs / deployment types
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.application.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'application_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function settings()
|
||||
{
|
||||
return $this->hasOne(ApplicationSetting::class);
|
||||
@@ -123,6 +196,36 @@ class Application extends BaseModel
|
||||
}
|
||||
);
|
||||
}
|
||||
public function dockerComposeLocation(): 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 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(
|
||||
@@ -157,7 +260,16 @@ class Application extends BaseModel
|
||||
: explode(',', $this->ports_exposes)
|
||||
);
|
||||
}
|
||||
|
||||
public function serviceType()
|
||||
{
|
||||
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
|
||||
return str($this->image)->before(':')->value() === $service;
|
||||
})->first());
|
||||
if ($found->isNotEmpty()) {
|
||||
return $found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function environment_variables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
|
||||
@@ -224,7 +336,6 @@ class Application extends BaseModel
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function isDeploymentInprogress()
|
||||
{
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
|
||||
@@ -334,12 +445,583 @@ class Application extends BaseModel
|
||||
}
|
||||
public function isMultipleServerDeployment()
|
||||
{
|
||||
if (isDev()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public function healthCheckUrl()
|
||||
{
|
||||
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
return null;
|
||||
}
|
||||
if (!$this->health_check_port) {
|
||||
$health_check_port = $this->ports_exposes_array[0];
|
||||
} else {
|
||||
$health_check_port = $this->health_check_port;
|
||||
}
|
||||
if ($this->health_check_path) {
|
||||
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
|
||||
} else {
|
||||
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
|
||||
}
|
||||
return $full_healthcheck_url;
|
||||
}
|
||||
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
|
||||
];
|
||||
}
|
||||
function generateBaseDir(string $uuid)
|
||||
{
|
||||
return "/artifacts/{$uuid}";
|
||||
}
|
||||
function generateHealthCheckCommands()
|
||||
{
|
||||
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
|
||||
return 'exit 0';
|
||||
}
|
||||
if (!$this->health_check_port) {
|
||||
$health_check_port = $this->ports_exposes_array[0];
|
||||
} else {
|
||||
$health_check_port = $this->health_check_port;
|
||||
}
|
||||
if ($this->health_check_path) {
|
||||
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path} > /dev/null"
|
||||
];
|
||||
} else {
|
||||
$this->full_healthcheck_url = "{$this->health_check_method}: {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->health_check_method} -f {$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/"
|
||||
];
|
||||
}
|
||||
return implode(' ', $generated_healthchecks_commands);
|
||||
}
|
||||
function generateLocalPersistentVolumes(int $pullRequestId)
|
||||
{
|
||||
$persistentStorages = [];
|
||||
$volumeNames = [];
|
||||
foreach ($this->persistentStorages as $persistentStorage) {
|
||||
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
|
||||
if ($pullRequestId !== 0) {
|
||||
$volume_name = $volume_name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
$persistentStorages[] = $volume_name . ':' . $persistentStorage->mount_path;
|
||||
|
||||
if ($persistentStorage->host_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = $persistentStorage->name;
|
||||
|
||||
if ($pullRequestId !== 0) {
|
||||
$name = $name . '-pr-' . $pullRequestId;
|
||||
}
|
||||
|
||||
$volumeNames[$name] = [
|
||||
'name' => $name,
|
||||
'external' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'persistentStorages' => $persistentStorages,
|
||||
'volumeNames' => $volumeNames,
|
||||
];
|
||||
}
|
||||
public function generateEnvironmentVariables($ports)
|
||||
{
|
||||
$environmentVariables = collect();
|
||||
// ray('Generate Environment Variables')->green();
|
||||
if ($this->pull_request_id === 0) {
|
||||
// ray($this->runtime_environment_variables)->green();
|
||||
foreach ($this->runtime_environment_variables as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->nixpacks_environment_variables as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
} else {
|
||||
// ray($this->runtime_environment_variables_preview)->green();
|
||||
foreach ($this->runtime_environment_variables_preview as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
foreach ($this->nixpacks_environment_variables_preview as $env) {
|
||||
$environmentVariables->push("$env->key=$env->value");
|
||||
}
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($environmentVariables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
|
||||
$environmentVariables->push("PORT={$ports[0]}");
|
||||
}
|
||||
return $environmentVariables->all();
|
||||
}
|
||||
function generateDockerComposeFile(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
|
||||
{
|
||||
$pullRequestId = $deployment->pull_request_id;
|
||||
$ports = $this->settings->is_static ? [80] : $this->ports_exposes_array;
|
||||
$container_name = generateApplicationContainerName($this, $this->pull_request_id);
|
||||
$commit = str($deployment->getOutput('git_commit_sha'))->before("\t");
|
||||
|
||||
[
|
||||
'productionImageName' => $productionImageName
|
||||
] = $this->generateImageNames($commit, $pullRequestId);
|
||||
|
||||
[
|
||||
'persistentStorages' => $persistentStorages,
|
||||
'volumeNames' => $volumeNames
|
||||
] = $this->generateLocalPersistentVolumes($pullRequestId);
|
||||
|
||||
$environmentVariables = $this->generateEnvironmentVariables($ports);
|
||||
|
||||
if (data_get($this, 'custom_labels')) {
|
||||
$labels = collect(str($this->custom_labels)->explode(','));
|
||||
$labels = $labels->filter(function ($value, $key) {
|
||||
return !Str::startsWith($value, 'coolify.');
|
||||
});
|
||||
$this->custom_labels = $labels->implode(',');
|
||||
$this->save();
|
||||
} else {
|
||||
$labels = collect(generateLabelsApplication($this, $this->preview));
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$labels = collect(generateLabelsApplication($this, $this->preview));
|
||||
}
|
||||
$labels = $labels->merge(defaultLabels($this->id, $this->uuid, $this->pull_request_id))->toArray();
|
||||
$docker_compose = [
|
||||
'version' => '3.8',
|
||||
'services' => [
|
||||
$container_name => [
|
||||
'image' => $productionImageName,
|
||||
'container_name' => $container_name,
|
||||
'restart' => RESTART_MODE,
|
||||
'environment' => $environmentVariables,
|
||||
'expose' => $ports,
|
||||
'networks' => [
|
||||
$this->destination->network,
|
||||
],
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generateHealthCheckCommands()
|
||||
],
|
||||
'interval' => $this->health_check_interval . 's',
|
||||
'timeout' => $this->health_check_timeout . 's',
|
||||
'retries' => $this->health_check_retries,
|
||||
'start_period' => $this->health_check_start_period . 's'
|
||||
],
|
||||
'mem_limit' => $this->limits_memory,
|
||||
'memswap_limit' => $this->limits_memory_swap,
|
||||
'mem_swappiness' => $this->limits_memory_swappiness,
|
||||
'mem_reservation' => $this->limits_memory_reservation,
|
||||
'cpus' => (int) $this->limits_cpus,
|
||||
'cpuset' => $this->limits_cpuset,
|
||||
'cpu_shares' => $this->limits_cpu_shares,
|
||||
]
|
||||
],
|
||||
'networks' => [
|
||||
$this->destination->network => [
|
||||
'external' => true,
|
||||
'name' => $this->destination->network,
|
||||
'attachable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
if ($server->isSwarm()) {
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.container_name');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.expose');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.restart');
|
||||
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_limit');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.memswap_limit');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_swappiness');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.mem_reservation');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpus');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpuset');
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.cpu_shares');
|
||||
|
||||
$docker_compose['services'][$container_name]['deploy'] = [
|
||||
'placement' => [
|
||||
'constraints' => [
|
||||
'node.role == worker'
|
||||
]
|
||||
],
|
||||
'mode' => 'replicated',
|
||||
'replicas' => 1,
|
||||
'update_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'rollback_config' => [
|
||||
'order' => 'start-first'
|
||||
],
|
||||
'labels' => $labels,
|
||||
'resources' => [
|
||||
'limits' => [
|
||||
'cpus' => $this->limits_cpus,
|
||||
'memory' => $this->limits_memory,
|
||||
],
|
||||
'reservations' => [
|
||||
'cpus' => $this->limits_cpus,
|
||||
'memory' => $this->limits_memory,
|
||||
]
|
||||
]
|
||||
];
|
||||
} else {
|
||||
$docker_compose['services'][$container_name]['labels'] = $labels;
|
||||
}
|
||||
if ($server->isLogDrainEnabled() && $this->isLogDrainEnabled()) {
|
||||
$docker_compose['services'][$container_name]['logging'] = [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
];
|
||||
}
|
||||
if ($this->settings->is_gpu_enabled) {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'] = [
|
||||
[
|
||||
'driver' => data_get($this, 'settings.gpu_driver', 'nvidia'),
|
||||
'capabilities' => ['gpu'],
|
||||
'options' => data_get($this, 'settings.gpu_options', [])
|
||||
]
|
||||
];
|
||||
if (data_get($this, 'settings.gpu_count')) {
|
||||
$count = data_get($this, 'settings.gpu_count');
|
||||
if ($count === 'all') {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
|
||||
} else {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
|
||||
}
|
||||
} else if (data_get($this, 'settings.gpu_device_ids')) {
|
||||
$docker_compose['services'][$container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this, 'settings.gpu_device_ids');
|
||||
}
|
||||
}
|
||||
if ($this->isHealthcheckDisabled()) {
|
||||
data_forget($docker_compose, 'services.' . $container_name . '.healthcheck');
|
||||
}
|
||||
if (count($this->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
|
||||
$docker_compose['services'][$container_name]['ports'] = $this->ports_mappings_array;
|
||||
}
|
||||
if (count($persistentStorages) > 0) {
|
||||
$docker_compose['services'][$container_name]['volumes'] = $persistentStorages;
|
||||
}
|
||||
if (count($volumeNames) > 0) {
|
||||
$docker_compose['volumes'] = $volumeNames;
|
||||
}
|
||||
|
||||
$docker_compose['services'][$this->uuid] = $docker_compose['services'][$container_name];
|
||||
|
||||
data_forget($docker_compose, 'services.' . $container_name);
|
||||
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => executeInDocker($deployment->deployment_uuid, "echo '{$docker_compose_base64}' | base64 -d > {$workdir}/docker-compose.yml"),
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
function rollingUpdateApplication(Server $server, ApplicationDeploymentQueue $deployment, string $workdir)
|
||||
{
|
||||
$pullRequestId = $deployment->pull_request_id;
|
||||
$containerName = generateApplicationContainerName($this, $pullRequestId);
|
||||
// if (count($this->ports_mappings_array) > 0) {
|
||||
// $deployment->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->id, $pullRequestId);
|
||||
ray($containers);
|
||||
// if ($pullRequestId === 0) {
|
||||
// $containers = $containers->filter(function ($container) use ($containerName) {
|
||||
// return data_get($container, 'Names') !== $containerName;
|
||||
// });
|
||||
// }
|
||||
$containers->each(function ($container) use ($server, $deployment) {
|
||||
$removingContainerName = data_get($container, 'Names');
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => "docker rm -f $removingContainerName",
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
});
|
||||
// }
|
||||
$server->executeRemoteCommand(
|
||||
commands: collect([])->push([
|
||||
'command' => executeInDocker($deployment->deployment_uuid, "docker compose --project-directory {$workdir} up --build -d"),
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
$deployment->addLogEntry("New container started.");
|
||||
}
|
||||
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
|
||||
{
|
||||
$baseDir = $this->generateBaseDir($deployment_uuid);
|
||||
if ($this->git_commit_sha !== 'HEAD') {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
|
||||
}
|
||||
if ($this->settings->is_git_submodules_enabled) {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git submodule update --init --recursive";
|
||||
}
|
||||
if ($this->settings->is_git_lfs_enabled) {
|
||||
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git lfs pull";
|
||||
}
|
||||
return $git_clone_command;
|
||||
}
|
||||
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
|
||||
{
|
||||
$branch = $this->git_branch;
|
||||
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
|
||||
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
|
||||
$commands = collect([]);
|
||||
$git_clone_command = "git clone -b {$this->git_branch}";
|
||||
if ($only_checkout) {
|
||||
$git_clone_command = "git clone --no-checkout -b {$this->git_branch}";
|
||||
}
|
||||
if ($pull_request_id !== 0) {
|
||||
$pr_branch_name = "pr-{$pull_request_id}-coolify";
|
||||
}
|
||||
|
||||
if ($this->deploymentType() === 'source') {
|
||||
$source_html_url = data_get($this, 'source.html_url');
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
|
||||
$source_html_url_host = $url['host'];
|
||||
$source_html_url_scheme = $url['scheme'];
|
||||
|
||||
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($this->source->is_public) {
|
||||
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
|
||||
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
|
||||
if (!$only_checkout) {
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
}
|
||||
} else {
|
||||
$github_access_token = generate_github_installation_token($this->source);
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}"));
|
||||
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
|
||||
} else {
|
||||
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}");
|
||||
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
|
||||
}
|
||||
}
|
||||
if ($pull_request_id !== 0) {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"));
|
||||
} else {
|
||||
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name");
|
||||
}
|
||||
}
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($this->deploymentType() === 'deploy_key') {
|
||||
$fullRepoUrl = $customRepository;
|
||||
$private_key = data_get($this, 'private_key.private_key');
|
||||
if (is_null($private_key)) {
|
||||
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
|
||||
}
|
||||
$private_key = base64_encode($private_key);
|
||||
$git_clone_command_base = "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_clone_command} {$customRepository} {$baseDir}";
|
||||
if (!$only_checkout) {
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands = collect([
|
||||
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),
|
||||
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
|
||||
executeInDocker($deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"mkdir -p /root/.ssh",
|
||||
"echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa",
|
||||
"chmod 600 /root/.ssh/id_rsa",
|
||||
]);
|
||||
}
|
||||
if ($pull_request_id !== 0) {
|
||||
if ($git_type === 'gitlab') {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$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 && git checkout $pr_branch_name";
|
||||
}
|
||||
if ($git_type === 'github') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
} else {
|
||||
$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 && git checkout $pr_branch_name";
|
||||
}
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
}
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl
|
||||
];
|
||||
}
|
||||
if ($this->deploymentType() === 'other') {
|
||||
$fullRepoUrl = $customRepository;
|
||||
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
}
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl
|
||||
];
|
||||
}
|
||||
}
|
||||
public function prepareHelperImage(string $deploymentUuid)
|
||||
{
|
||||
$basedir = $this->generateBaseDir($deploymentUuid);
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$server = data_get($this, 'destination.server');
|
||||
$network = data_get($this, 'destination.network');
|
||||
|
||||
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
|
||||
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
|
||||
|
||||
$commands = collect([]);
|
||||
if ($dockerConfigFileExists === 'OK') {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
|
||||
"hidden" => true,
|
||||
]);
|
||||
}
|
||||
$commands->push([
|
||||
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
|
||||
"hidden" => true,
|
||||
]);
|
||||
return $commands;
|
||||
}
|
||||
function parseCompose(int $pull_request_id = 0)
|
||||
{
|
||||
if ($this->docker_compose_raw) {
|
||||
$mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id);
|
||||
if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) {
|
||||
parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true);
|
||||
}
|
||||
return $mainCompose;
|
||||
} else {
|
||||
return collect([]);
|
||||
}
|
||||
}
|
||||
function loadComposeFile($isInit = false)
|
||||
{
|
||||
$initialDockerComposeLocation = $this->docker_compose_location;
|
||||
// $initialDockerComposePrLocation = $this->docker_compose_pr_location;
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
$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");
|
||||
// }
|
||||
$commands = collect([
|
||||
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
|
||||
$cloneCommand,
|
||||
"git sparse-checkout init --cone",
|
||||
"git sparse-checkout set {$fileList->implode(' ')}",
|
||||
"git read-tree -mu HEAD",
|
||||
"cat .$workdir$composeFile",
|
||||
]);
|
||||
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
|
||||
if (!$composeFileContent) {
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->save();
|
||||
throw new \Exception("Could not load base compose file from $workdir$composeFile");
|
||||
} else {
|
||||
$this->docker_compose_raw = $composeFileContent;
|
||||
$this->save();
|
||||
}
|
||||
// if ($composeFile === $prComposeFile) {
|
||||
// $this->docker_compose_pr_raw = $composeFileContent;
|
||||
// $this->save();
|
||||
// } else {
|
||||
// $commands = collect([
|
||||
// "cd /tmp/{$uuid}",
|
||||
// "cat .$workdir$prComposeFile",
|
||||
// ]);
|
||||
// $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
|
||||
// if (!$composePrFileContent) {
|
||||
// $this->docker_compose_pr_location = $initialDockerComposePrLocation;
|
||||
// $this->save();
|
||||
// throw new \Exception("Could not load compose file from $workdir$prComposeFile");
|
||||
// } else {
|
||||
// $this->docker_compose_pr_raw = $composePrFileContent;
|
||||
// $this->save();
|
||||
// }
|
||||
// }
|
||||
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
]);
|
||||
instant_remote_process($commands, $this->destination->server, false);
|
||||
return [
|
||||
'parsedServices' => $this->parseCompose(),
|
||||
'initialDockerComposeLocation' => $this->docker_compose_location,
|
||||
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,53 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ApplicationDeploymentQueue extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function setStatus(string $status) {
|
||||
$this->update([
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
public function getOutput($name)
|
||||
{
|
||||
if (!$this->logs) {
|
||||
return null;
|
||||
}
|
||||
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
|
||||
}
|
||||
|
||||
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
|
||||
{
|
||||
if ($type === 'error') {
|
||||
$type = 'stderr';
|
||||
}
|
||||
$message = str($message)->trim();
|
||||
if ($message->startsWith('╔')) {
|
||||
$message = "\n" . $message;
|
||||
}
|
||||
$newLogEntry = [
|
||||
'command' => null,
|
||||
'output' => remove_iip($message),
|
||||
'type' => $type,
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
'batch' => 1,
|
||||
];
|
||||
if ($this->logs) {
|
||||
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$newLogEntry['order'] = count($previousLogs) + 1;
|
||||
$previousLogs[] = $newLogEntry;
|
||||
$this->update([
|
||||
'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
} else {
|
||||
$this->update([
|
||||
'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,26 @@ namespace App\Models;
|
||||
class ApplicationPreview extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($preview) {
|
||||
if ($preview->application->build_pack === 'dockercompose') {
|
||||
$server = $preview->application->destination->server;
|
||||
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
|
||||
$volumes = data_get($composeFile, 'volumes');
|
||||
$networks = data_get($composeFile, 'networks');
|
||||
$networkKeys = collect($networks)->keys();
|
||||
$volumeKeys = collect($volumes)->keys();
|
||||
$volumeKeys->each(function ($key) use ($server) {
|
||||
instant_remote_process(["docker volume rm -f $key"], $server, false);
|
||||
});
|
||||
$networkKeys->each(function ($key) use ($server) {
|
||||
instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm $key"], $server, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
|
||||
{
|
||||
return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail();
|
||||
|
||||
@@ -16,29 +16,16 @@ class ApplicationSetting extends Model
|
||||
'is_git_submodules_enabled' => 'boolean',
|
||||
'is_git_lfs_enabled' => 'boolean',
|
||||
];
|
||||
protected $fillable = [
|
||||
'application_id',
|
||||
'is_static',
|
||||
'is_auto_deploy_enabled',
|
||||
'is_force_https_enabled',
|
||||
'is_debug_enabled',
|
||||
'is_preview_deployments_enabled',
|
||||
'is_git_submodules_enabled',
|
||||
'is_git_lfs_enabled',
|
||||
];
|
||||
protected $guarded = [];
|
||||
|
||||
public function isStatic(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (is_null($this->application->ports_exposes)) {
|
||||
if ($value) {
|
||||
$this->application->ports_exposes = '80';
|
||||
} else {
|
||||
$this->application->ports_exposes = '3000';
|
||||
}
|
||||
$this->application->save();
|
||||
if ($value) {
|
||||
$this->application->ports_exposes = 80;
|
||||
}
|
||||
$this->application->save();
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class InstanceSettings extends Model implements SendsEmail
|
||||
{
|
||||
@@ -16,6 +18,18 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
'smtp_password' => 'encrypted',
|
||||
];
|
||||
|
||||
public function fqdn(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if ($value) {
|
||||
$url = Url::fromString($value);
|
||||
$host = $url->getHost();
|
||||
return $url->getScheme() . '://' . $host;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
public static function get()
|
||||
{
|
||||
return InstanceSettings::findOrFail(0);
|
||||
|
||||
@@ -2,22 +2,26 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Actions\Server\InstallNewRelic;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Notifications\Server\Revived;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
|
||||
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use SchemalessAttributesTrait;
|
||||
public static $batch_counter = 0;
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
@@ -31,25 +35,10 @@ class Server extends BaseModel
|
||||
}
|
||||
$server->forceFill($payload);
|
||||
});
|
||||
|
||||
static::created(function ($server) {
|
||||
ServerSetting::create([
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
if ($server->id === 0) {
|
||||
StandaloneDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
@@ -78,7 +67,7 @@ class Server extends BaseModel
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name');
|
||||
return Server::whereTeamId($teamId)->with('settings', 'swarmDockers', 'standaloneDockers')->select($selectArray->all())->orderBy('name');
|
||||
}
|
||||
|
||||
static public function isUsable()
|
||||
@@ -97,7 +86,40 @@ class Server extends BaseModel
|
||||
{
|
||||
return $this->hasOne(ServerSetting::class);
|
||||
}
|
||||
|
||||
public function addInitialNetwork()
|
||||
{
|
||||
if ($this->id === 0) {
|
||||
if ($this->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'coolify',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if ($this->isSwarm()) {
|
||||
SwarmDocker::create([
|
||||
'name' => 'coolify-overlay',
|
||||
'network' => 'coolify-overlay',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
} else {
|
||||
StandaloneDocker::create([
|
||||
'name' => 'coolify-overlay',
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function proxyType()
|
||||
{
|
||||
$proxyType = $this->proxy->get('type');
|
||||
@@ -189,6 +211,13 @@ class Server extends BaseModel
|
||||
{
|
||||
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
|
||||
}
|
||||
public function definedResources()
|
||||
{
|
||||
$applications = $this->applications();
|
||||
$databases = $this->databases();
|
||||
$services = $this->services();
|
||||
return $applications->concat($databases)->concat($services->get());
|
||||
}
|
||||
public function hasDefinedResources()
|
||||
{
|
||||
$applications = $this->applications()->count() > 0;
|
||||
@@ -217,6 +246,23 @@ class Server extends BaseModel
|
||||
return $standaloneDocker->applications;
|
||||
})->flatten();
|
||||
}
|
||||
public function dockerComposeBasedApplications()
|
||||
{
|
||||
return $this->applications()->filter(function ($application) {
|
||||
return data_get($application, 'build_pack') === 'dockercompose';
|
||||
});
|
||||
}
|
||||
public function dockerComposeBasedPreviewDeployments()
|
||||
{
|
||||
return $this->previews()->filter(function ($preview) {
|
||||
$applicationId = data_get($preview, 'application_id');
|
||||
$application = Application::find($applicationId);
|
||||
if (!$application) {
|
||||
return false;
|
||||
}
|
||||
return data_get($application, 'build_pack') === 'dockercompose';
|
||||
});
|
||||
}
|
||||
public function services()
|
||||
{
|
||||
return $this->hasMany(Service::class);
|
||||
@@ -302,9 +348,9 @@ class Server extends BaseModel
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
|
||||
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled || $this->settings->is_logdrain_custom_enabled;
|
||||
}
|
||||
public function validateOS()
|
||||
public function validateOS(): bool | Stringable
|
||||
{
|
||||
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
|
||||
$datas = collect(explode("\n", $os_release));
|
||||
@@ -314,41 +360,49 @@ class Server extends BaseModel
|
||||
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
|
||||
}
|
||||
$ID = data_get($collectedData, 'ID');
|
||||
$ID_LIKE = data_get($collectedData, 'ID_LIKE');
|
||||
$VERSION_ID = data_get($collectedData, 'VERSION_ID');
|
||||
// ray($ID, $ID_LIKE, $VERSION_ID);
|
||||
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
|
||||
// $ID_LIKE = data_get($collectedData, 'ID_LIKE');
|
||||
// $VERSION_ID = data_get($collectedData, 'VERSION_ID');
|
||||
$supported = collect(SUPPORTED_OS)->filter(function ($supportedOs) use ($ID) {
|
||||
if (str($supportedOs)->contains($ID)) {
|
||||
return str($ID);
|
||||
}
|
||||
});
|
||||
if ($supported->count() === 1) {
|
||||
ray('supported');
|
||||
return str($ID_LIKE)->explode(' ')->first();
|
||||
return str($supported->first());
|
||||
} else {
|
||||
ray('not supported');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public function isSwarm()
|
||||
{
|
||||
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
|
||||
}
|
||||
public function validateConnection()
|
||||
{
|
||||
if ($this->skipServer()) {
|
||||
$server = Server::find($this->id);
|
||||
if ($server->skipServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$uptime = instant_remote_process(['uptime'], $this, false);
|
||||
$uptime = instant_remote_process(['uptime'], $server, false);
|
||||
if (!$uptime) {
|
||||
$this->settings()->update([
|
||||
$server->settings()->update([
|
||||
'is_reachable' => false,
|
||||
]);
|
||||
return false;
|
||||
} else {
|
||||
$this->settings()->update([
|
||||
$server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
]);
|
||||
$this->update([
|
||||
$server->update([
|
||||
'unreachable_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
if (data_get($this, 'unreachable_notification_sent') === true) {
|
||||
$this->team->notify(new Revived($this));
|
||||
$this->update(['unreachable_notification_sent' => false]);
|
||||
if (data_get($server, 'unreachable_notification_sent') === true) {
|
||||
$server->team->notify(new Revived($server));
|
||||
$server->update(['unreachable_notification_sent' => false]);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -366,7 +420,20 @@ class Server extends BaseModel
|
||||
}
|
||||
$this->settings->is_usable = true;
|
||||
$this->settings->save();
|
||||
$this->validateCoolifyNetwork();
|
||||
$this->validateCoolifyNetwork(isSwarm: false);
|
||||
return true;
|
||||
}
|
||||
public function validateDockerSwarm()
|
||||
{
|
||||
$swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);
|
||||
$swarmStatus = str($swarmStatus)->trim()->after(':')->trim();
|
||||
if ($swarmStatus === 'inactive') {
|
||||
throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.');
|
||||
return false;
|
||||
}
|
||||
$this->settings->is_usable = true;
|
||||
$this->settings->save();
|
||||
$this->validateCoolifyNetwork(isSwarm: true);
|
||||
return true;
|
||||
}
|
||||
public function validateDockerEngineVersion()
|
||||
@@ -383,8 +450,161 @@ class Server extends BaseModel
|
||||
$this->settings->save();
|
||||
return true;
|
||||
}
|
||||
public function validateCoolifyNetwork()
|
||||
public function validateCoolifyNetwork($isSwarm = false)
|
||||
{
|
||||
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
|
||||
if ($isSwarm) {
|
||||
return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
|
||||
} else {
|
||||
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
|
||||
}
|
||||
}
|
||||
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
foreach ($commands as $command) {
|
||||
$realCommand = data_get($command, 'command');
|
||||
if (is_null($realCommand)) {
|
||||
throw new \RuntimeException('Command is not set');
|
||||
}
|
||||
$hidden = data_get($command, 'hidden', false);
|
||||
$ignoreErrors = data_get($command, 'ignoreErrors', false);
|
||||
$customOutputType = data_get($command, 'customOutputType');
|
||||
$name = data_get($command, 'name');
|
||||
$remoteCommand = generateSshCommand($this, $realCommand);
|
||||
|
||||
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
|
||||
$output = str($output)->trim();
|
||||
if ($output->startsWith('╔')) {
|
||||
$output = "\n" . $output;
|
||||
}
|
||||
$newLogEntry = [
|
||||
'command' => remove_iip($realCommand),
|
||||
'output' => remove_iip($output),
|
||||
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
'batch' => static::$batch_counter,
|
||||
];
|
||||
if ($loggingModel) {
|
||||
if (!$loggingModel->logs) {
|
||||
$newLogEntry['order'] = 1;
|
||||
} else {
|
||||
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$newLogEntry['order'] = count($previousLogs) + 1;
|
||||
}
|
||||
if ($name) {
|
||||
$newLogEntry['name'] = $name;
|
||||
}
|
||||
|
||||
$previousLogs[] = $newLogEntry;
|
||||
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
|
||||
$loggingModel->save();
|
||||
}
|
||||
});
|
||||
if ($loggingModel) {
|
||||
$loggingModel->update([
|
||||
'current_process_id' => $process->id(),
|
||||
]);
|
||||
}
|
||||
$processResult = $process->wait();
|
||||
if ($processResult->exitCode() !== 0) {
|
||||
if (!$ignoreErrors) {
|
||||
if ($loggingModel) {
|
||||
$status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$loggingModel->status = $status;
|
||||
$loggingModel->save();
|
||||
}
|
||||
throw new \RuntimeException($processResult->errorOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
|
||||
{
|
||||
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
|
||||
$containers = $containers->filter(function ($container) use ($containerName) {
|
||||
return data_get($container, 'Names') !== $containerName;
|
||||
});
|
||||
$containers->each(function ($container) {
|
||||
$removableContainer = data_get($container, 'Names');
|
||||
$this->server->executeRemoteCommand(
|
||||
commands: collect([
|
||||
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
|
||||
'hidden' => true,
|
||||
'ignoreErrors' => true
|
||||
]),
|
||||
loggingModel: $this->deploymentQueueEntry
|
||||
);
|
||||
});
|
||||
}
|
||||
public function getHostIPMappings($network)
|
||||
{
|
||||
$addHosts = null;
|
||||
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $this);
|
||||
if (!is_null($allContainers)) {
|
||||
$allContainers = format_docker_command_output_to_json($allContainers);
|
||||
$ips = collect([]);
|
||||
if (count($allContainers) > 0) {
|
||||
$allContainers = $allContainers[0];
|
||||
foreach ($allContainers as $container) {
|
||||
$containerName = data_get($container, 'Name');
|
||||
if ($containerName === 'coolify-proxy') {
|
||||
continue;
|
||||
}
|
||||
$containerIp = data_get($container, 'IPv4Address');
|
||||
if ($containerName && $containerIp) {
|
||||
$containerIp = str($containerIp)->before('/');
|
||||
$ips->put($containerName, $containerIp->value());
|
||||
}
|
||||
}
|
||||
}
|
||||
$addHosts = $ips->map(function ($ip, $name) {
|
||||
return "--add-host $name:$ip";
|
||||
})->implode(' ');
|
||||
}
|
||||
return $addHosts;
|
||||
}
|
||||
public function checkIfDockerImageExists(string $imageName, ApplicationDeploymentQueue $deployment)
|
||||
{
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"name" => "local_image_found",
|
||||
"command" => "docker images -q {$imageName} 2>/dev/null",
|
||||
"hidden" => true,
|
||||
]
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
if (str($deployment->getOutput('local_image_found'))->isEmpty()) {
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"command" => "docker pull {$imageName} 2>/dev/null",
|
||||
"ignoreErrors" => true,
|
||||
"hidden" => true
|
||||
],
|
||||
[
|
||||
"name" => "local_image_found",
|
||||
"command" => "docker images -q {$imageName} 2>/dev/null",
|
||||
"hidden" => true,
|
||||
]
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
}
|
||||
public function createWorkDirForDeployment(string $workdir, ApplicationDeploymentQueue $deployment)
|
||||
{
|
||||
$this->executeRemoteCommand(
|
||||
commands: collect([
|
||||
[
|
||||
"command" => executeInDocker($deployment->deployment_uuid, "mkdir -p {$workdir}"),
|
||||
"ignoreErrors" => true,
|
||||
"hidden" => true
|
||||
],
|
||||
]),
|
||||
loggingModel: $deployment
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class Service extends BaseModel
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('minio'):
|
||||
case str($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();
|
||||
@@ -105,7 +105,7 @@ class Service extends BaseModel
|
||||
|
||||
$fields->put('MinIO', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('weblate'):
|
||||
case str($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();
|
||||
@@ -130,8 +130,84 @@ class Service extends BaseModel
|
||||
]);
|
||||
}
|
||||
$fields->put('Weblate', $data);
|
||||
break;
|
||||
case str($image)?->contains('meilisearch'):
|
||||
$data = collect([]);
|
||||
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
|
||||
if ($SERVICE_PASSWORD_MEILISEARCH) {
|
||||
$data = $data->merge([
|
||||
'API Key' => [
|
||||
'key' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'key'),
|
||||
'value' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'value'),
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Meilisearch', $data);
|
||||
break;
|
||||
case str($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();
|
||||
$MAIL_OPTIONS_SECURE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SECURE')->first();
|
||||
$MAIL_OPTIONS_PORT = $this->environment_variables()->where('key', 'MAIL_OPTIONS_PORT')->first();
|
||||
$MAIL_OPTIONS_SERVICE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SERVICE')->first();
|
||||
$MAIL_OPTIONS_HOST = $this->environment_variables()->where('key', 'MAIL_OPTIONS_HOST')->first();
|
||||
if ($MAIL_OPTIONS_AUTH_PASS) {
|
||||
$data = $data->merge([
|
||||
'Mail Password' => [
|
||||
'key' => data_get($MAIL_OPTIONS_AUTH_PASS, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_AUTH_PASS, 'value'),
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($MAIL_OPTIONS_AUTH_USER) {
|
||||
$data = $data->merge([
|
||||
'Mail User' => [
|
||||
'key' => data_get($MAIL_OPTIONS_AUTH_USER, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_AUTH_USER, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($MAIL_OPTIONS_SECURE) {
|
||||
$data = $data->merge([
|
||||
'Mail Secure' => [
|
||||
'key' => data_get($MAIL_OPTIONS_SECURE, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_SECURE, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($MAIL_OPTIONS_PORT) {
|
||||
$data = $data->merge([
|
||||
'Mail Port' => [
|
||||
'key' => data_get($MAIL_OPTIONS_PORT, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_PORT, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($MAIL_OPTIONS_SERVICE) {
|
||||
$data = $data->merge([
|
||||
'Mail Service' => [
|
||||
'key' => data_get($MAIL_OPTIONS_SERVICE, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_SERVICE, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($MAIL_OPTIONS_HOST) {
|
||||
$data = $data->merge([
|
||||
'Mail Host' => [
|
||||
'key' => data_get($MAIL_OPTIONS_HOST, 'key'),
|
||||
'value' => data_get($MAIL_OPTIONS_HOST, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$fields->put('Ghost', $data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ray($fields);
|
||||
$databases = $this->databases()->get();
|
||||
|
||||
foreach ($databases as $database) {
|
||||
@@ -300,6 +376,17 @@ class Service extends BaseModel
|
||||
}
|
||||
}
|
||||
}
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.service.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'service_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function documentation()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
@@ -371,521 +458,12 @@ class Service extends BaseModel
|
||||
|
||||
public function parse(bool $isNew = false): Collection
|
||||
{
|
||||
// ray()->clearAll();
|
||||
if ($this->docker_compose_raw) {
|
||||
try {
|
||||
$yaml = Yaml::parse($this->docker_compose_raw);
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception($e->getMessage());
|
||||
}
|
||||
|
||||
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
|
||||
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
|
||||
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
|
||||
$services = data_get($yaml, 'services');
|
||||
|
||||
$generatedServiceFQDNS = collect([]);
|
||||
if (is_null($this->destination)) {
|
||||
$destination = $this->server->destinations()->first();
|
||||
if ($destination) {
|
||||
$this->destination()->associate($destination);
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
$definedNetwork = collect([$this->uuid]);
|
||||
|
||||
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
|
||||
$serviceVolumes = collect(data_get($service, 'volumes', []));
|
||||
$servicePorts = collect(data_get($service, 'ports', []));
|
||||
$serviceNetworks = collect(data_get($service, 'networks', []));
|
||||
$serviceVariables = collect(data_get($service, 'environment', []));
|
||||
$serviceLabels = collect(data_get($service, 'labels', []));
|
||||
if ($serviceLabels->count() > 0) {
|
||||
$removedLabels = collect([]);
|
||||
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
|
||||
if (!str($serviceLabel)->contains('=')) {
|
||||
$removedLabels->put($serviceLabelName, $serviceLabel);
|
||||
return false;
|
||||
}
|
||||
return $serviceLabel;
|
||||
});
|
||||
foreach($removedLabels as $removedLabelName =>$removedLabel) {
|
||||
$serviceLabels->push("$removedLabelName=$removedLabel");
|
||||
}
|
||||
}
|
||||
|
||||
$containerName = "$serviceName-{$this->uuid}";
|
||||
|
||||
// Decide if the service is a database
|
||||
$isDatabase = false;
|
||||
$image = data_get_str($service, 'image');
|
||||
if ($image->contains(':')) {
|
||||
$image = Str::of($image);
|
||||
} else {
|
||||
$image = Str::of($image)->append(':latest');
|
||||
}
|
||||
$imageName = $image->before(':');
|
||||
|
||||
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
|
||||
$isDatabase = true;
|
||||
}
|
||||
data_set($service, 'is_database', $isDatabase);
|
||||
|
||||
// Create new serviceApplication or serviceDatabase
|
||||
if ($isDatabase) {
|
||||
if ($isNew) {
|
||||
$savedService = ServiceDatabase::create([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $this->id
|
||||
]);
|
||||
} else {
|
||||
$savedService = ServiceDatabase::where([
|
||||
'name' => $serviceName,
|
||||
'service_id' => $this->id
|
||||
])->first();
|
||||
}
|
||||
} else {
|
||||
if ($isNew) {
|
||||
$savedService = ServiceApplication::create([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $this->id
|
||||
]);
|
||||
} else {
|
||||
$savedService = ServiceApplication::where([
|
||||
'name' => $serviceName,
|
||||
'service_id' => $this->id
|
||||
])->first();
|
||||
}
|
||||
}
|
||||
if (is_null($savedService)) {
|
||||
if ($isDatabase) {
|
||||
$savedService = ServiceDatabase::create([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $this->id
|
||||
]);
|
||||
} else {
|
||||
$savedService = ServiceApplication::create([
|
||||
'name' => $serviceName,
|
||||
'image' => $image,
|
||||
'service_id' => $this->id
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if image changed
|
||||
if ($savedService->image !== $image) {
|
||||
$savedService->image = $image;
|
||||
$savedService->save();
|
||||
}
|
||||
|
||||
// Collect/create/update networks
|
||||
if ($serviceNetworks->count() > 0) {
|
||||
foreach ($serviceNetworks as $networkName => $networkDetails) {
|
||||
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
|
||||
return $value == $networkName || $key == $networkName;
|
||||
});
|
||||
if (!$networkExists) {
|
||||
$topLevelNetworks->put($networkDetails, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect/create/update ports
|
||||
$collectedPorts = collect([]);
|
||||
if ($servicePorts->count() > 0) {
|
||||
foreach ($servicePorts as $sport) {
|
||||
if (is_string($sport) || is_numeric($sport)) {
|
||||
$collectedPorts->push($sport);
|
||||
}
|
||||
if (is_array($sport)) {
|
||||
$target = data_get($sport, 'target');
|
||||
$published = data_get($sport, 'published');
|
||||
$protocol = data_get($sport, 'protocol');
|
||||
$collectedPorts->push("$target:$published/$protocol");
|
||||
}
|
||||
}
|
||||
}
|
||||
$savedService->ports = $collectedPorts->implode(',');
|
||||
$savedService->save();
|
||||
|
||||
// Add Coolify specific networks
|
||||
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
|
||||
return $value == $definedNetwork;
|
||||
});
|
||||
if (!$definedNetworkExists) {
|
||||
foreach ($definedNetwork as $network) {
|
||||
$topLevelNetworks->put($network, [
|
||||
'name' => $network,
|
||||
'external' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
$networks->put($serviceNetwork, null);
|
||||
} else if (gettype($serviceNetwork) === 'array') {
|
||||
// networks:
|
||||
// default:
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
}
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
$networks->put($network, null);
|
||||
}
|
||||
data_set($service, 'networks', $networks->toArray());
|
||||
|
||||
// Collect/create/update volumes
|
||||
if ($serviceVolumes->count() > 0) {
|
||||
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
|
||||
$type = null;
|
||||
$source = null;
|
||||
$target = null;
|
||||
$content = null;
|
||||
$isDirectory = false;
|
||||
if (is_string($volume)) {
|
||||
$source = Str::of($volume)->before(':');
|
||||
$target = Str::of($volume)->after(':')->beforeLast(':');
|
||||
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
|
||||
$type = Str::of('bind');
|
||||
} else {
|
||||
$type = Str::of('volume');
|
||||
}
|
||||
} else if (is_array($volume)) {
|
||||
$type = data_get_str($volume, 'type');
|
||||
$source = data_get_str($volume, 'source');
|
||||
$target = data_get_str($volume, 'target');
|
||||
$content = data_get($volume, 'content');
|
||||
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
|
||||
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
|
||||
if ($foundConfig) {
|
||||
$contentNotNull = data_get($foundConfig, 'content');
|
||||
if ($contentNotNull) {
|
||||
$content = $contentNotNull;
|
||||
}
|
||||
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
|
||||
}
|
||||
}
|
||||
if ($type->value() === 'bind') {
|
||||
if ($source->value() === "/var/run/docker.sock") {
|
||||
return $volume;
|
||||
}
|
||||
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
|
||||
return $volume;
|
||||
}
|
||||
LocalFileVolume::updateOrCreate(
|
||||
[
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $savedService->id,
|
||||
'resource_type' => get_class($savedService)
|
||||
],
|
||||
[
|
||||
'fs_path' => $source,
|
||||
'mount_path' => $target,
|
||||
'content' => $content,
|
||||
'is_directory' => $isDirectory,
|
||||
'resource_id' => $savedService->id,
|
||||
'resource_type' => get_class($savedService)
|
||||
]
|
||||
);
|
||||
} else if ($type->value() === 'volume') {
|
||||
$slugWithoutUuid = Str::slug($source, '-');
|
||||
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
|
||||
if (is_string($volume)) {
|
||||
$source = Str::of($volume)->before(':');
|
||||
$target = Str::of($volume)->after(':')->beforeLast(':');
|
||||
$source = $name;
|
||||
$volume = "$source:$target";
|
||||
} else if (is_array($volume)) {
|
||||
data_set($volume, 'source', $name);
|
||||
}
|
||||
$topLevelVolumes->put($name, [
|
||||
'name' => $name,
|
||||
]);
|
||||
LocalPersistentVolume::updateOrCreate(
|
||||
[
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $savedService->id,
|
||||
'resource_type' => get_class($savedService)
|
||||
],
|
||||
[
|
||||
'name' => $name,
|
||||
'mount_path' => $target,
|
||||
'resource_id' => $savedService->id,
|
||||
'resource_type' => get_class($savedService)
|
||||
]
|
||||
);
|
||||
}
|
||||
$savedService->getFilesFromServer(isInit: true);
|
||||
return $volume;
|
||||
});
|
||||
data_set($service, 'volumes', $serviceVolumes->toArray());
|
||||
}
|
||||
|
||||
// Add env_file with at least .env to the service
|
||||
// $envFile = collect(data_get($service, 'env_file', []));
|
||||
// if ($envFile->count() > 0) {
|
||||
// if (!$envFile->contains('.env')) {
|
||||
// $envFile->push('.env');
|
||||
// }
|
||||
// } else {
|
||||
// $envFile = collect(['.env']);
|
||||
// }
|
||||
// data_set($service, 'env_file', $envFile->toArray());
|
||||
|
||||
|
||||
// Get variables from the service
|
||||
foreach ($serviceVariables as $variableName => $variable) {
|
||||
if (is_numeric($variableName)) {
|
||||
$variable = Str::of($variable);
|
||||
if ($variable->contains('=')) {
|
||||
// - SESSION_SECRET=123
|
||||
// - SESSION_SECRET=
|
||||
$key = $variable->before('=');
|
||||
$value = $variable->after('=');
|
||||
} else {
|
||||
// - SESSION_SECRET
|
||||
$key = $variable;
|
||||
$value = null;
|
||||
}
|
||||
} else {
|
||||
// SESSION_SECRET: 123
|
||||
// SESSION_SECRET:
|
||||
$key = Str::of($variableName);
|
||||
$value = Str::of($variable);
|
||||
}
|
||||
// TODO: here is the problem
|
||||
if ($key->startsWith('SERVICE_FQDN')) {
|
||||
if ($isNew || $savedService->fqdn === null) {
|
||||
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
|
||||
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
|
||||
if (substr_count($key->value(), '_') === 3) {
|
||||
// SERVICE_FQDN_UMAMI_1000
|
||||
$port = $key->afterLast('_');
|
||||
} else {
|
||||
// SERVICE_FQDN_UMAMI
|
||||
$port = null;
|
||||
}
|
||||
if ($port) {
|
||||
$fqdn = "$fqdn:$port";
|
||||
}
|
||||
if (substr_count($key->value(), '_') >= 2) {
|
||||
if (is_null($value)) {
|
||||
$value = Str::of('/');
|
||||
}
|
||||
$path = $value->value();
|
||||
if ($generatedServiceFQDNS->count() > 0) {
|
||||
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
|
||||
if ($alreadyGenerated) {
|
||||
$fqdn = $generatedServiceFQDNS->get($key->value());
|
||||
} else {
|
||||
$generatedServiceFQDNS->put($key->value(), $fqdn);
|
||||
}
|
||||
} else {
|
||||
$generatedServiceFQDNS->put($key->value(), $fqdn);
|
||||
}
|
||||
$fqdn = "$fqdn$path";
|
||||
}
|
||||
|
||||
if (!$isDatabase) {
|
||||
if ($savedService->fqdn) {
|
||||
$fqdn = $savedService->fqdn . ',' . $fqdn;
|
||||
} else {
|
||||
$fqdn = $fqdn;
|
||||
}
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
}
|
||||
// data_forget($service, "environment.$variableName");
|
||||
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
|
||||
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
|
||||
// $yaml = data_forget($yaml, "services.$serviceName.environment");
|
||||
// }
|
||||
continue;
|
||||
}
|
||||
if ($value?->startsWith('$')) {
|
||||
$value = Str::of(replaceVariables($value));
|
||||
$key = $value;
|
||||
$foundEnv = EnvironmentVariable::where([
|
||||
'key' => $key,
|
||||
'service_id' => $this->id,
|
||||
])->first();
|
||||
if ($value->startsWith('SERVICE_')) {
|
||||
// Count _ in $value
|
||||
$count = substr_count($value->value(), '_');
|
||||
if ($count === 2) {
|
||||
// SERVICE_FQDN_UMAMI
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$forService = $value->afterLast('_');
|
||||
$generatedValue = null;
|
||||
$port = null;
|
||||
}
|
||||
if ($count === 3) {
|
||||
// SERVICE_FQDN_UMAMI_1000
|
||||
$command = $value->after('SERVICE_')->before('_');
|
||||
$forService = $value->after('SERVICE_')->after('_')->before('_');
|
||||
$generatedValue = null;
|
||||
$port = $value->afterLast('_');
|
||||
}
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
if (Str::lower($forService) === $serviceName) {
|
||||
$fqdn = generateFqdn($this->server, $containerName);
|
||||
} else {
|
||||
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
|
||||
}
|
||||
if ($port) {
|
||||
$fqdn = "$fqdn:$port";
|
||||
}
|
||||
if ($foundEnv) {
|
||||
$fqdn = data_get($foundEnv, 'value');
|
||||
} else {
|
||||
if ($command->value() === 'URL') {
|
||||
$fqdn = Str::of($fqdn)->after('://')->value();
|
||||
}
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $fqdn,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
if (!$isDatabase) {
|
||||
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch ($command) {
|
||||
case 'PASSWORD':
|
||||
$generatedValue = Str::password(symbols: false);
|
||||
break;
|
||||
case 'PASSWORD_64':
|
||||
$generatedValue = Str::password(length: 64, symbols: false);
|
||||
break;
|
||||
case 'BASE64_64':
|
||||
$generatedValue = Str::random(64);
|
||||
break;
|
||||
case 'BASE64_128':
|
||||
$generatedValue = Str::random(128);
|
||||
break;
|
||||
case 'BASE64':
|
||||
$generatedValue = Str::random(32);
|
||||
break;
|
||||
case 'USER':
|
||||
$generatedValue = Str::random(16);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$foundEnv) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $generatedValue,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($value->contains(':-')) {
|
||||
$key = $value->before(':');
|
||||
$defaultValue = $value->after(':-');
|
||||
} else if ($value->contains('-')) {
|
||||
$key = $value->before('-');
|
||||
$defaultValue = $value->after('-');
|
||||
} else if ($value->contains(':?')) {
|
||||
$key = $value->before(':');
|
||||
$defaultValue = $value->after(':?');
|
||||
} else if ($value->contains('?')) {
|
||||
$key = $value->before('?');
|
||||
$defaultValue = $value->after('?');
|
||||
} else {
|
||||
$key = $value;
|
||||
$defaultValue = null;
|
||||
}
|
||||
if ($foundEnv) {
|
||||
$defaultValue = data_get($foundEnv, 'value');
|
||||
}
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'key' => $key,
|
||||
'service_id' => $this->id,
|
||||
], [
|
||||
'value' => $defaultValue,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add labels to the service
|
||||
if ($savedService->serviceType()) {
|
||||
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
|
||||
} else {
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
}
|
||||
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
|
||||
$serviceLabels = $serviceLabels->merge($defaultLabels);
|
||||
if (!$isDatabase && $fqdns->count() > 0) {
|
||||
if ($fqdns) {
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
|
||||
}
|
||||
}
|
||||
if ($this->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
|
||||
data_set($service, 'logging', [
|
||||
'driver' => 'fluentd',
|
||||
'options' => [
|
||||
'fluentd-address' => "tcp://127.0.0.1:24224",
|
||||
'fluentd-async' => "true",
|
||||
'fluentd-sub-second-precision' => "true",
|
||||
]
|
||||
]);
|
||||
}
|
||||
data_set($service, 'labels', $serviceLabels->toArray());
|
||||
data_forget($service, 'is_database');
|
||||
data_set($service, 'restart', RESTART_MODE);
|
||||
data_set($service, 'container_name', $containerName);
|
||||
data_forget($service, 'volumes.*.content');
|
||||
data_forget($service, 'volumes.*.isDirectory');
|
||||
// Remove unnecessary variables from service.environment
|
||||
// $withoutServiceEnvs = collect([]);
|
||||
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
|
||||
// ray($key, $value);
|
||||
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
|
||||
// $k = Str::of($value)->before("=");
|
||||
// $v = Str::of($value)->after("=");
|
||||
// $withoutServiceEnvs->put($k->value(), $v->value());
|
||||
// }
|
||||
// });
|
||||
// ray($withoutServiceEnvs);
|
||||
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
|
||||
return $service;
|
||||
});
|
||||
$finalServices = [
|
||||
'version' => $dockerComposeVersion,
|
||||
'services' => $services->toArray(),
|
||||
'volumes' => $topLevelVolumes->toArray(),
|
||||
'networks' => $topLevelNetworks->toArray(),
|
||||
];
|
||||
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
|
||||
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
|
||||
$this->save();
|
||||
$this->saveComposeConfigs();
|
||||
return collect([]);
|
||||
} else {
|
||||
return collect([]);
|
||||
}
|
||||
return parseDockerComposeFile($this, $isNew);
|
||||
}
|
||||
public function networks()
|
||||
{
|
||||
$networks = getTopLevelNetworks($this);
|
||||
// ray($networks);
|
||||
return $networks;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,17 @@ class StandaloneMariadb extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.database.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'database_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
|
||||
@@ -48,6 +48,17 @@ class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
}
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.database.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'database_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function mongoInitdbRootPassword(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -41,6 +41,17 @@ class StandaloneMysql extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.database.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'database_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-mysql';
|
||||
|
||||
@@ -41,7 +41,17 @@ class StandalonePostgresql extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.database.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'database_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
|
||||
@@ -36,7 +36,17 @@ class StandaloneRedis extends BaseModel
|
||||
$database->environment_variables()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function link()
|
||||
{
|
||||
if (data_get($this, 'environment.project.uuid')) {
|
||||
return route('project.database.configuration', [
|
||||
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
||||
'environment_name' => data_get($this, 'environment.name'),
|
||||
'database_uuid' => data_get($this, 'uuid')
|
||||
]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function isLogDrainEnabled()
|
||||
{
|
||||
return data_get($this, 'is_log_drain_enabled', false);
|
||||
|
||||
@@ -4,13 +4,57 @@ namespace App\Models;
|
||||
|
||||
class SwarmDocker extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return $this->morphMany(Application::class, 'destination');
|
||||
}
|
||||
|
||||
public function postgresqls()
|
||||
{
|
||||
return $this->morphMany(StandalonePostgresql::class, 'destination');
|
||||
}
|
||||
|
||||
public function redis()
|
||||
{
|
||||
return $this->morphMany(StandaloneRedis::class, 'destination');
|
||||
}
|
||||
public function mongodbs()
|
||||
{
|
||||
return $this->morphMany(StandaloneMongodb::class, 'destination');
|
||||
}
|
||||
public function mysqls()
|
||||
{
|
||||
return $this->morphMany(StandaloneMysql::class, 'destination');
|
||||
}
|
||||
public function mariadbs()
|
||||
{
|
||||
return $this->morphMany(StandaloneMariadb::class, 'destination');
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
public function services()
|
||||
{
|
||||
return $this->morphMany(Service::class, 'destination');
|
||||
}
|
||||
|
||||
public function databases()
|
||||
{
|
||||
$postgresqls = $this->postgresqls;
|
||||
$redis = $this->redis;
|
||||
$mongodbs = $this->mongodbs;
|
||||
$mysqls = $this->mysqls;
|
||||
$mariadbs = $this->mariadbs;
|
||||
return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
|
||||
}
|
||||
|
||||
public function attachedTo()
|
||||
{
|
||||
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,15 @@ use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvi
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event to listener mappings for the application.
|
||||
*
|
||||
* @var array<class-string, array<int, class-string>>
|
||||
*/
|
||||
protected $listen = [
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
// Registered::class => [
|
||||
// SendEmailVerificationNotification::class,
|
||||
// ],
|
||||
];
|
||||
|
||||
/**
|
||||
* Register any events for your application.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if events and listeners should be automatically discovered.
|
||||
*/
|
||||
public function shouldDiscoverEvents(): bool
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -12,6 +12,7 @@ use Illuminate\Support\Str;
|
||||
trait ExecuteRemoteCommand
|
||||
{
|
||||
public ?string $save = null;
|
||||
public static int $batch_counter = 0;
|
||||
public function execute_remote_command(...$commands)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
@@ -23,8 +24,6 @@ trait ExecuteRemoteCommand
|
||||
if ($this->server instanceof Server === false) {
|
||||
throw new \RuntimeException('Server is not set or is not an instance of Server model');
|
||||
}
|
||||
|
||||
|
||||
$commandsText->each(function ($single_command) {
|
||||
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
|
||||
if ($command === null) {
|
||||
@@ -49,32 +48,29 @@ trait ExecuteRemoteCommand
|
||||
'hidden' => $hidden,
|
||||
'batch' => static::$batch_counter,
|
||||
];
|
||||
|
||||
if (!$this->log_model->logs) {
|
||||
if (!$this->application_deployment_queue->logs) {
|
||||
$new_log_entry['order'] = 1;
|
||||
} else {
|
||||
$previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
|
||||
$new_log_entry['order'] = count($previous_logs) + 1;
|
||||
}
|
||||
|
||||
$previous_logs[] = $new_log_entry;
|
||||
$this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
|
||||
$this->log_model->save();
|
||||
$this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
|
||||
$this->application_deployment_queue->save();
|
||||
|
||||
if ($this->save) {
|
||||
$this->saved_outputs[$this->save] = Str::of($output)->trim();
|
||||
}
|
||||
});
|
||||
$this->log_model->update([
|
||||
$this->application_deployment_queue->update([
|
||||
'current_process_id' => $process->id(),
|
||||
]);
|
||||
|
||||
$process_result = $process->wait();
|
||||
if ($process_result->exitCode() !== 0) {
|
||||
if (!$ignore_errors) {
|
||||
$status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$this->log_model->status = $status;
|
||||
$this->log_model->save();
|
||||
$this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
|
||||
$this->application_deployment_queue->save();
|
||||
throw new \RuntimeException($process_result->errorOutput());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Jobs\ApplicationDeployDockerImageJob;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Jobs\ApplicationDeploymentNewJob;
|
||||
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
|
||||
use App\Jobs\ApplicationRestartJob;
|
||||
use App\Jobs\MultipleApplicationDeploymentJob;
|
||||
@@ -11,7 +12,7 @@ use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
|
||||
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $is_new_deployment = false)
|
||||
{
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => $application_id,
|
||||
@@ -36,46 +37,34 @@ function queue_application_deployment(int $application_id, string $deployment_uu
|
||||
if ($running_deployments->count() > 0) {
|
||||
return;
|
||||
}
|
||||
// New deployment
|
||||
// dispatchDeploymentJob($deployment->id);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
|
||||
if ($is_new_deployment) {
|
||||
dispatch(new ApplicationDeploymentNewJob(
|
||||
deployment: $deployment,
|
||||
application: Application::find($application_id)
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
} else {
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $deployment->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
}
|
||||
}
|
||||
|
||||
function queue_next_deployment(Application $application)
|
||||
function queue_next_deployment(Application $application, bool $isNew = false)
|
||||
{
|
||||
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
|
||||
if ($next_found) {
|
||||
// New deployment
|
||||
// dispatchDeploymentJob($next_found->id);
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
|
||||
if ($isNew) {
|
||||
dispatch(new ApplicationDeploymentNewJob(
|
||||
deployment: $next_found,
|
||||
application: $application
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
} else {
|
||||
dispatch(new ApplicationDeploymentJob(
|
||||
application_deployment_queue_id: $next_found->id,
|
||||
))->onConnection('long-running')->onQueue('long-running');
|
||||
}
|
||||
}
|
||||
}
|
||||
function dispatchDeploymentJob($id)
|
||||
{
|
||||
$applicationQueue = ApplicationDeploymentQueue::find($id);
|
||||
$application = Application::find($applicationQueue->application_id);
|
||||
|
||||
$isRestartOnly = data_get($applicationQueue, 'restart_only');
|
||||
$isSimpleDockerFile = data_get($application, 'dockerfile');
|
||||
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
|
||||
|
||||
if ($isRestartOnly) {
|
||||
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else if ($isSimpleDockerFile) {
|
||||
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else if ($isDockerImage) {
|
||||
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
|
||||
} else {
|
||||
throw new Exception('Unknown build pack');
|
||||
}
|
||||
}
|
||||
|
||||
// Deployment things
|
||||
function generateHostIpMapping(Server $server, string $network)
|
||||
{
|
||||
@@ -122,12 +111,12 @@ function prepareHelperContainer(Server $server, string $network, string $deploym
|
||||
$commands = collect([]);
|
||||
if ($dockerConfigFileExists === 'OK') {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
|
||||
"command" => "docker run -d --network $network --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
|
||||
"hidden" => true,
|
||||
]);
|
||||
} else {
|
||||
$commands->push([
|
||||
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
|
||||
"command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
|
||||
"hidden" => true,
|
||||
]);
|
||||
}
|
||||
@@ -201,7 +190,6 @@ function generateComposeFile(string $deploymentUuid, Server $server, string $net
|
||||
];
|
||||
}
|
||||
if ($application->settings->is_gpu_enabled) {
|
||||
ray('asd');
|
||||
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
|
||||
[
|
||||
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
|
||||
@@ -302,39 +290,37 @@ function generateEnvironmentVariables(Application $application, $ports, int $pul
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
function rollingUpdate(Application $application, string $deploymentUuid)
|
||||
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
|
||||
{
|
||||
$commands = collect([]);
|
||||
$workDir = generateWorkdir($deploymentUuid, $application);
|
||||
if (count($application->ports_mappings_array) > 0) {
|
||||
// $this->execute_remote_command(
|
||||
// [
|
||||
// "echo '\n----------------------------------------'",
|
||||
// ],
|
||||
// ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
|
||||
// );
|
||||
// $this->stop_running_container(force: true);
|
||||
// $this->start_by_compose_file();
|
||||
if ($application->build_pack === 'dockerimage') {
|
||||
$loggingModel->addLogEntry('Pulling latest images from the registry.');
|
||||
$commands->push(
|
||||
[
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
|
||||
"hidden" => true
|
||||
],
|
||||
[
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
|
||||
"hidden" => true
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$commands->push(
|
||||
[
|
||||
"command" => "echo '\n----------------------------------------'"
|
||||
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
|
||||
"hidden" => true
|
||||
],
|
||||
[
|
||||
"command" => "echo -n 'Rolling update started.'"
|
||||
]
|
||||
);
|
||||
if ($application->build_pack === 'dockerimage') {
|
||||
$commands->push(
|
||||
["echo -n 'Pulling latest images from the registry.'"],
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
} else {
|
||||
$commands->push(
|
||||
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
return $commands;
|
||||
}
|
||||
return $commands;
|
||||
}
|
||||
function removeOldDeployment(string $containerName)
|
||||
{
|
||||
$commands = collect([]);
|
||||
$commands->push(
|
||||
["docker rm -f $containerName >/dev/null 2>&1"],
|
||||
);
|
||||
return $commands;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
];
|
||||
|
||||
// Based on /etc/os-release
|
||||
const SUPPORTED_OS = [
|
||||
'debian',
|
||||
'rhel centos fedora'
|
||||
'ubuntu debian raspbian',
|
||||
'centos fedora rhel ol rocky',
|
||||
'sles opensuse-leap opensuse-tumbleweed'
|
||||
];
|
||||
|
||||
@@ -24,7 +24,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
|
||||
}
|
||||
return StandalonePostgresql::create([
|
||||
'name' => generate_database_name('postgresql'),
|
||||
'postgres_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
@@ -39,7 +39,7 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone
|
||||
}
|
||||
return StandaloneRedis::create([
|
||||
'name' => generate_database_name('redis'),
|
||||
'redis_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
@@ -54,7 +54,7 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo
|
||||
}
|
||||
return StandaloneMongodb::create([
|
||||
'name' => generate_database_name('mongodb'),
|
||||
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
@@ -68,8 +68,8 @@ function create_standalone_mysql($environment_id, $destination_uuid): Standalone
|
||||
}
|
||||
return StandaloneMysql::create([
|
||||
'name' => generate_database_name('mysql'),
|
||||
'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'mysql_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
@@ -83,8 +83,8 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
|
||||
}
|
||||
return StandaloneMariadb::create([
|
||||
'name' => generate_database_name('mariadb'),
|
||||
'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'mariadb_password' => \Illuminate\Support\Str::password(symbols: false),
|
||||
'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
|
||||
'environment_id' => $environment_id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
@@ -90,7 +93,11 @@ function executeInDocker(string $containerId, string $command)
|
||||
|
||||
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
|
||||
{
|
||||
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
|
||||
if ($server->isSwarm()) {
|
||||
$container = instant_remote_process(["docker service ls --filter 'name={$container_id}' --format '{{json .}}' "], $server, $throwError);
|
||||
} else {
|
||||
$container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
|
||||
}
|
||||
if (!$container) {
|
||||
return 'exited';
|
||||
}
|
||||
@@ -98,7 +105,19 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
|
||||
if ($all_data) {
|
||||
return $container[0];
|
||||
}
|
||||
return data_get($container[0], 'State.Status', 'exited');
|
||||
if ($server->isSwarm()) {
|
||||
$replicas = data_get($container[0], 'Replicas');
|
||||
$replicas = explode('/', $replicas);
|
||||
$active = (int)$replicas[0];
|
||||
$total = (int)$replicas[1];
|
||||
if ($active === $total) {
|
||||
return 'running';
|
||||
} else {
|
||||
return 'starting';
|
||||
}
|
||||
} else {
|
||||
return data_get($container[0], 'State.Status', 'exited');
|
||||
}
|
||||
}
|
||||
|
||||
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
|
||||
@@ -137,18 +156,28 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
|
||||
$labels->push('coolify.name=' . $name);
|
||||
$labels->push('coolify.pullRequestId=' . $pull_request_id);
|
||||
if ($type === 'service') {
|
||||
$labels->push('coolify.service.subId=' . $subId);
|
||||
$labels->push('coolify.service.subType=' . $subType);
|
||||
$subId && $labels->push('coolify.service.subId=' . $subId);
|
||||
$subType && $labels->push('coolify.service.subType=' . $subType);
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
function generateServiceSpecificFqdns(ServiceApplication|Application $resource, $forTraefik = false)
|
||||
{
|
||||
$variables = collect($service->service->environment_variables);
|
||||
$type = $service->serviceType();
|
||||
if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
|
||||
$uuid = $resource->uuid;
|
||||
$server = $resource->service->server;
|
||||
$environment_variables = $resource->service->environment_variables;
|
||||
$type = $resource->serviceType();
|
||||
} else if ($resource->getMorphClass() === 'App\Models\Application') {
|
||||
$uuid = $resource->uuid;
|
||||
$server = $resource->destination->server;
|
||||
$environment_variables = $resource->environment_variables;
|
||||
$type = $resource->serviceType();
|
||||
}
|
||||
$variables = collect($environment_variables);
|
||||
$payload = collect([]);
|
||||
switch ($type) {
|
||||
case $type->contains('minio'):
|
||||
case $type?->contains('minio'):
|
||||
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
|
||||
@@ -156,12 +185,12 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
}
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
|
||||
$MINIO_BROWSER_REDIRECT_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
|
||||
"value" => generateFqdn($server, 'console-' . $uuid)
|
||||
]);
|
||||
}
|
||||
if (is_null($MINIO_SERVER_URL?->value)) {
|
||||
$MINIO_SERVER_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
|
||||
"value" => generateFqdn($server, 'minio-' . $uuid)
|
||||
]);
|
||||
}
|
||||
if ($forTraefik) {
|
||||
@@ -175,10 +204,11 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
$MINIO_SERVER_URL->value,
|
||||
]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
|
||||
{
|
||||
$labels = collect([]);
|
||||
$labels->push('traefik.enable=true');
|
||||
@@ -260,3 +290,21 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
|
||||
}
|
||||
return $labels->all();
|
||||
}
|
||||
|
||||
function isDatabaseImage(?string $image = null)
|
||||
{
|
||||
if (is_null($image)) {
|
||||
return false;
|
||||
}
|
||||
$image = str($image);
|
||||
if ($image->contains(':')) {
|
||||
$image = str($image);
|
||||
} else {
|
||||
$image = str($image)->append(':latest');
|
||||
}
|
||||
$imageName = $image->before(':');
|
||||
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Proxy\SaveConfiguration;
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
@@ -12,9 +13,32 @@ function get_proxy_path()
|
||||
}
|
||||
function connectProxyToNetworks(Server $server)
|
||||
{
|
||||
// Standalone networks
|
||||
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
||||
return $docker['network'];
|
||||
})->unique();
|
||||
});
|
||||
// Service networks
|
||||
foreach ($server->services()->get() as $service) {
|
||||
$networks->push($service->networks());
|
||||
}
|
||||
// Docker compose based apps
|
||||
$docker_compose_apps = $server->dockerComposeBasedApplications();
|
||||
foreach ($docker_compose_apps as $app) {
|
||||
$networks->push($app->uuid);
|
||||
}
|
||||
// Docker compose based preview deployments
|
||||
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
|
||||
foreach ($docker_compose_previews as $preview) {
|
||||
$pullRequestId = $preview->pull_request_id;
|
||||
$applicationId = $preview->application_id;
|
||||
$application = Application::find($applicationId);
|
||||
if (!$application) {
|
||||
continue;
|
||||
}
|
||||
$network = "{$application->uuid}-{$pullRequestId}";
|
||||
$networks->push($network);
|
||||
}
|
||||
$networks = collect($networks)->flatten()->unique();
|
||||
if ($networks->count() === 0) {
|
||||
$networks = collect(['coolify']);
|
||||
}
|
||||
@@ -30,9 +54,15 @@ function connectProxyToNetworks(Server $server)
|
||||
function generate_default_proxy_configuration(Server $server)
|
||||
{
|
||||
$proxy_path = get_proxy_path();
|
||||
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
||||
return $docker['network'];
|
||||
})->unique();
|
||||
if ($server->isSwarm()) {
|
||||
$networks = collect($server->swarmDockers)->map(function ($docker) {
|
||||
return $docker['network'];
|
||||
})->unique();
|
||||
} else {
|
||||
$networks = collect($server->standaloneDockers)->map(function ($docker) {
|
||||
return $docker['network'];
|
||||
})->unique();
|
||||
}
|
||||
if ($networks->count() === 0) {
|
||||
$networks = collect(['coolify']);
|
||||
}
|
||||
@@ -42,6 +72,16 @@ function generate_default_proxy_configuration(Server $server)
|
||||
"external" => true,
|
||||
];
|
||||
});
|
||||
$labels = [
|
||||
"traefik.enable=true",
|
||||
"traefik.http.routers.traefik.entrypoints=http",
|
||||
"traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
|
||||
"traefik.http.routers.traefik.service=api@internal",
|
||||
"traefik.http.services.traefik.loadbalancer.server.port=8080",
|
||||
// Global Middlewares
|
||||
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
|
||||
"traefik.http.middlewares.gzip.compress=true",
|
||||
];
|
||||
$config = [
|
||||
"version" => "3.8",
|
||||
"networks" => $array_of_networks->toArray(),
|
||||
@@ -78,7 +118,6 @@ function generate_default_proxy_configuration(Server $server)
|
||||
"--entrypoints.https.address=:443",
|
||||
"--entrypoints.http.http.encodequerysemicolons=true",
|
||||
"--entrypoints.https.http.encodequerysemicolons=true",
|
||||
"--providers.docker=true",
|
||||
"--providers.docker.exposedbydefault=false",
|
||||
"--providers.file.directory=/traefik/dynamic/",
|
||||
"--providers.file.watch=true",
|
||||
@@ -86,16 +125,7 @@ function generate_default_proxy_configuration(Server $server)
|
||||
"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json",
|
||||
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
|
||||
],
|
||||
"labels" => [
|
||||
"traefik.enable=true",
|
||||
"traefik.http.routers.traefik.entrypoints=http",
|
||||
"traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
|
||||
"traefik.http.routers.traefik.service=api@internal",
|
||||
"traefik.http.services.traefik.loadbalancer.server.port=8080",
|
||||
// Global Middlewares
|
||||
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
|
||||
"traefik.http.middlewares.gzip.compress=true",
|
||||
],
|
||||
"labels" => $labels,
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -104,7 +134,24 @@ function generate_default_proxy_configuration(Server $server)
|
||||
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
|
||||
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
|
||||
}
|
||||
$config = Yaml::dump($config, 4, 2);
|
||||
if ($server->isSwarm()) {
|
||||
data_forget($config, 'services.traefik.container_name');
|
||||
data_forget($config, 'services.traefik.restart');
|
||||
data_forget($config, 'services.traefik.labels');
|
||||
|
||||
$config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true";
|
||||
$config['services']['traefik']['deploy'] = [
|
||||
"labels" => $labels,
|
||||
"placement" => [
|
||||
"constraints" => [
|
||||
"node.role==manager",
|
||||
],
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$config['services']['traefik']['command'][] = "--providers.docker=true";
|
||||
}
|
||||
$config = Yaml::dump($config, 12, 2);
|
||||
SaveConfiguration::run($server, $config);
|
||||
return $config;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
|
||||
}
|
||||
return excludeCertainErrors($process->errorOutput(), $exitCode);
|
||||
}
|
||||
if ($output === 'null') {
|
||||
$output = null;
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
|
||||
@@ -151,6 +154,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
||||
if (is_null($application_deployment_queue)) {
|
||||
return collect([]);
|
||||
}
|
||||
// ray(data_get($application_deployment_queue, 'logs'));
|
||||
try {
|
||||
$decoded = json_decode(
|
||||
data_get($application_deployment_queue, 'logs'),
|
||||
@@ -160,14 +164,15 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
|
||||
} catch (\JsonException $exception) {
|
||||
return collect([]);
|
||||
}
|
||||
// ray($decoded );
|
||||
$formatted = collect($decoded);
|
||||
if (!$is_debug_enabled) {
|
||||
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
|
||||
}
|
||||
$formatted = $formatted
|
||||
->sortBy(fn ($i) => $i['order'])
|
||||
->sortBy(fn ($i) => data_get($i, 'order'))
|
||||
->map(function ($i) {
|
||||
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
|
||||
data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
|
||||
return $i;
|
||||
});
|
||||
return $formatted;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@
|
||||
"nubs/random-name-generator": "^2.2",
|
||||
"phpseclib/phpseclib": "~3.0",
|
||||
"poliander/cron": "^3.0",
|
||||
"pusher/pusher-php-server": "^7.2",
|
||||
"resend/resend-laravel": "^0.5.0",
|
||||
"sentry/sentry-laravel": "^3.4",
|
||||
"spatie/laravel-activitylog": "^4.7.3",
|
||||
@@ -38,6 +39,7 @@
|
||||
"stripe/stripe-php": "^12.0",
|
||||
"symfony/yaml": "^6.2",
|
||||
"visus/cuid2": "^2.0.0",
|
||||
"wire-elements/modal": "^1.0",
|
||||
"yosymfony/toml": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
||||
1279
composer.lock
generated
1279
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -194,7 +194,7 @@ return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\FortifyServiceProvider::class,
|
||||
App\Providers\AuthServiceProvider::class,
|
||||
// App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('BROADCAST_DRIVER', 'null'),
|
||||
'default' => env('BROADCAST_DRIVER', 'pusher'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -32,13 +32,13 @@ return [
|
||||
|
||||
'pusher' => [
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
'key' => env('PUSHER_APP_KEY', 'coolify'),
|
||||
'secret' => env('PUSHER_APP_SECRET', 'coolify'),
|
||||
'app_id' => env('PUSHER_APP_ID', 'coolify'),
|
||||
'options' => [
|
||||
'host' => env('PUSHER_HOST') ?: 'api-' . env('PUSHER_APP_CLUSTER', 'mt1') . '.pusher.com',
|
||||
'port' => env('PUSHER_PORT', 443),
|
||||
'scheme' => env('PUSHER_SCHEME', 'https'),
|
||||
'host' => env('PUSHER_BACKEND_HOST', 'coolify-soketi'),
|
||||
'port' => env('PUSHER_BACKEND_PORT', 6001),
|
||||
'scheme' => env('PUSHER_SCHEME', 'http'),
|
||||
'encrypted' => true,
|
||||
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
||||
],
|
||||
|
||||
@@ -22,13 +22,13 @@ return [
|
||||
'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
|
||||
],
|
||||
'limits' => [
|
||||
'trial_period' => 7,
|
||||
'trial_period' => 0,
|
||||
'server' => [
|
||||
'zero' => 0,
|
||||
'self-hosted' => 999999999999,
|
||||
'basic' => 1,
|
||||
'pro' => 10,
|
||||
'ultimate' => 25,
|
||||
'basic' => env('LIMIT_SERVER_BASIC', 2),
|
||||
'pro' => env('LIMIT_SERVER_PRO', 10),
|
||||
'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25),
|
||||
],
|
||||
'email' => [
|
||||
'zero' => true,
|
||||
|
||||
52
config/livewire-ui-modal.php
Normal file
52
config/livewire-ui-modal.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include CSS
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The modal uses TailwindCSS, if you don't use TailwindCSS you will need
|
||||
| to set this parameter to true. This includes the modern-normalize css.
|
||||
|
|
||||
*/
|
||||
'include_css' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Include JS
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire UI will inject the required Javascript in your blade template.
|
||||
| If you want to bundle the required Javascript you can set this to false
|
||||
| and add `require('vendor/wire-elements/modal/resources/js/modal');`
|
||||
| to your script bundler like webpack.
|
||||
|
|
||||
*/
|
||||
'include_js' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Component Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure the default properties for a modal component.
|
||||
|
|
||||
| Supported modal_max_width
|
||||
| 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl'
|
||||
*/
|
||||
'component_defaults' => [
|
||||
'modal_max_width' => '7xl',
|
||||
|
||||
'close_modal_on_click_away' => true,
|
||||
|
||||
'close_modal_on_escape' => true,
|
||||
|
||||
'close_modal_on_escape_is_forceful' => true,
|
||||
|
||||
'dispatch_close_event' => false,
|
||||
|
||||
'destroy_on_close' => false,
|
||||
],
|
||||
];
|
||||
@@ -3,11 +3,11 @@
|
||||
return [
|
||||
|
||||
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
|
||||
'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568',
|
||||
'dsn' => 'https://bea22abf110618b07252032aa2e07859@o1082494.ingest.sentry.io/4505347448045568',
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.145',
|
||||
'release' => '4.0.0-beta.154',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.145';
|
||||
return '4.0.0-beta.154';
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->string('docker_compose_location')->nullable()->default('/docker-compose.yaml')->after('dockerfile_location');
|
||||
$table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location');
|
||||
|
||||
$table->longText('docker_compose')->nullable()->after('docker_compose_location');
|
||||
$table->longText('docker_compose_pr')->nullable()->after('docker_compose_location');
|
||||
$table->longText('docker_compose_raw')->nullable()->after('docker_compose');
|
||||
$table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose');
|
||||
|
||||
$table->text('docker_compose_domains')->nullable()->after('docker_compose_raw');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('docker_compose_location');
|
||||
$table->dropColumn('docker_compose_pr_location');
|
||||
$table->dropColumn('docker_compose');
|
||||
$table->dropColumn('docker_compose_pr');
|
||||
$table->dropColumn('docker_compose_raw');
|
||||
$table->dropColumn('docker_compose_pr_raw');
|
||||
$table->dropColumn('docker_compose_domains');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('swarm_dockers', function (Blueprint $table) {
|
||||
$table->string('network');
|
||||
|
||||
$table->unique(['server_id', 'network']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('swarm_dockers', function (Blueprint $table) {
|
||||
$table->dropColumn('network');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->renameColumn('is_part_of_swarm', 'is_swarm_manager');
|
||||
$table->boolean('is_swarm_worker')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->renameColumn('is_swarm_manager', 'is_part_of_swarm');
|
||||
$table->dropColumn('is_swarm_worker');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('service_databases', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('service_databases', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('standalone_mysqls', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('standalone_postgresqls', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('standalone_redis', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
Schema::table('standalone_mongodbs', function (Blueprint $table) {
|
||||
$table->dropColumn('is_include_timestamps');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_logdrain_custom_enabled')->default(false);
|
||||
$table->text('logdrain_custom_config')->nullable();
|
||||
$table->text('logdrain_custom_config_parser')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_logdrain_custom_enabled');
|
||||
$table->dropColumn('logdrain_custom_config');
|
||||
$table->dropColumn('logdrain_custom_config_parser');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -13,11 +13,11 @@ class StandaloneDockerSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// StandaloneDocker::create([
|
||||
// 'id' => 0,
|
||||
// 'name' => 'Standalone Docker 1',
|
||||
// 'network' => 'coolify',
|
||||
// 'server_id' => 0,
|
||||
// ]);
|
||||
StandaloneDocker::create([
|
||||
'id' => 0,
|
||||
'name' => 'Standalone Docker 1',
|
||||
'network' => 'coolify',
|
||||
'server_id' => 0,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
SwarmDocker::create([
|
||||
'name' => 'Swarm Docker 1',
|
||||
'server_id' => 1,
|
||||
]);
|
||||
// SwarmDocker::create([
|
||||
// 'name' => 'Swarm Docker 1',
|
||||
// 'server_id' => 1,
|
||||
// ]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ services:
|
||||
SSL_MODE: "off"
|
||||
AUTORUN_LARAVEL_STORAGE_LINK: "false"
|
||||
AUTORUN_LARAVEL_MIGRATION: "false"
|
||||
PUSHER_HOST: "${PUSHER_HOST}"
|
||||
PUSHER_PORT: "${PUSHER_PORT:-6001}"
|
||||
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
|
||||
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||
volumes:
|
||||
- .:/var/www/html/:cached
|
||||
postgres:
|
||||
@@ -44,9 +50,21 @@ services:
|
||||
volumes:
|
||||
- /data/coolify/_volumes/redis/:/data
|
||||
# - coolify-redis-data-dev:/data
|
||||
soketi:
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||
environment:
|
||||
SOKETI_DEBUG: "false"
|
||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||
vite:
|
||||
image: node:20
|
||||
working_dir: /var/www/html
|
||||
# environment:
|
||||
# VITE_PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
ports:
|
||||
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
|
||||
volumes:
|
||||
@@ -60,14 +78,14 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /data/coolify/:/data/coolify
|
||||
# - coolify-data-dev:/data/coolify
|
||||
remote-host:
|
||||
<<: *testing-host-base
|
||||
container_name: coolify-remote-host
|
||||
volumes:
|
||||
- /:/host
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /data/coolify/:/data/coolify
|
||||
# - coolify-data-dev:/data/coolify
|
||||
# remote-host:
|
||||
# <<: *testing-host-base
|
||||
# container_name: coolify-remote-host
|
||||
# volumes:
|
||||
# - /:/host
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# - /data/coolify/:/data/coolify
|
||||
# # - coolify-data-dev:/data/coolify
|
||||
mailpit:
|
||||
image: "axllent/mailpit:latest"
|
||||
container_name: coolify-mail
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
coolify:
|
||||
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.20}"
|
||||
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-4.0.0-beta.153}"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /data/coolify/source/.env
|
||||
@@ -28,11 +28,21 @@ services:
|
||||
- REDIS_HOST
|
||||
- REDIS_PASSWORD
|
||||
- HORIZON_MAX_PROCESSES
|
||||
- HORIZON_BALANCE_MAX_SHIFT
|
||||
- HORIZON_BALANCE_COOLDOWN
|
||||
- SSL_MODE=off
|
||||
- PHP_PM_CONTROL=dynamic
|
||||
- PHP_PM_START_SERVERS=1
|
||||
- PHP_PM_MIN_SPARE_SERVERS=1
|
||||
- PHP_PM_MAX_SPARE_SERVERS=10
|
||||
- PUSHER_HOST
|
||||
- PUSHER_BACKEND_HOST
|
||||
- PUSHER_PORT
|
||||
- PUSHER_BACKEND_PORT
|
||||
- PUSHER_SCHEME
|
||||
- PUSHER_APP_ID
|
||||
- PUSHER_APP_KEY
|
||||
- PUSHER_APP_SECRET
|
||||
- SELF_HOSTED
|
||||
- WAITLIST
|
||||
- SUBSCRIPTION_PROVIDER
|
||||
@@ -72,8 +82,8 @@ services:
|
||||
- "${APP_PORT:-8000}"
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:80/api/health || exit 1
|
||||
interval: 4s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
depends_on:
|
||||
postgres:
|
||||
@@ -95,8 +105,8 @@ services:
|
||||
"-d",
|
||||
"${DB_DATABASE:-coolify}"
|
||||
]
|
||||
interval: 2s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
redis:
|
||||
command: redis-server --save 20 1 --loglevel warning --requirepass ${REDIS_PASSWORD}
|
||||
@@ -106,8 +116,21 @@ services:
|
||||
- coolify-redis:/data
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 2s
|
||||
retries: 5
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
soketi:
|
||||
ports:
|
||||
- "${SOKETI_PORT:-6001}:6001"
|
||||
environment:
|
||||
SOKETI_DEBUG: "${SOKETI_DEBUG:-false}"
|
||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
healthcheck:
|
||||
test: wget -qO- http://localhost:6001/ready || exit 1
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
volumes:
|
||||
coolify-db:
|
||||
|
||||
@@ -24,6 +24,12 @@ services:
|
||||
restart: always
|
||||
networks:
|
||||
- coolify
|
||||
soketi:
|
||||
image: 'quay.io/soketi/soketi:1.6-16-alpine'
|
||||
container_name: coolify-soketi
|
||||
restart: always
|
||||
networks:
|
||||
- coolify
|
||||
networks:
|
||||
coolify:
|
||||
name: coolify
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/command/execlineb -P
|
||||
php /var/www/html/artisan app:init
|
||||
php /var/www/html/artisan app:init --cleanup
|
||||
|
||||
385
package-lock.json
generated
385
package-lock.json
generated
@@ -5,20 +5,24 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@alpinejs/focus": "^3.13.3",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"alpinejs": "3.13.2",
|
||||
"daisyui": "4.3.1",
|
||||
"alpinejs": "3.13.3",
|
||||
"daisyui": "4.4.19",
|
||||
"ioredis": "5.3.2",
|
||||
"tailwindcss-scrollbar": "0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "4.5.0",
|
||||
"@vitejs/plugin-vue": "4.5.1",
|
||||
"autoprefixer": "10.4.16",
|
||||
"axios": "1.6.2",
|
||||
"laravel-echo": "1.15.3",
|
||||
"laravel-vite-plugin": "0.8.1",
|
||||
"postcss": "8.4.31",
|
||||
"tailwindcss": "3.3.5",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.8"
|
||||
"postcss": "8.4.32",
|
||||
"pusher-js": "8.4.0-rc2",
|
||||
"tailwindcss": "3.3.6",
|
||||
"vite": "4.5.1",
|
||||
"vue": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
@@ -32,10 +36,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@alpinejs/focus": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.13.3.tgz",
|
||||
"integrity": "sha512-fTRX/9wOfysyZ1PJ4gHeUnmiNTIgqBDIqKxeP5iMvj1UHD3TFLDXllvoIKH3ezqcsyQZqxd/q1MFM7dlIhkmeg==",
|
||||
"dependencies": {
|
||||
"focus-trap": "^6.9.4",
|
||||
"tabbable": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
|
||||
"integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
|
||||
"version": "7.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
|
||||
"integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -396,6 +409,11 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
@@ -503,9 +521,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz",
|
||||
"integrity": "sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.1.tgz",
|
||||
"integrity": "sha512-DaUzYFr+2UGDG7VSSdShKa9sIWYBa1LL8KC0MNOf2H5LjcTPjob0x8LbkqXWmAtbANJCkpiQTj66UVcQkN2s3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
@@ -516,77 +534,77 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz",
|
||||
"integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.10.tgz",
|
||||
"integrity": "sha512-doe0hODR1+i1menPkRzJ5MNR6G+9uiZHIknK3Zn5OcIztu6GGw7u0XUzf3AgB8h/dfsZC9eouzoLo3c3+N/cVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/shared": "3.3.8",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@vue/shared": "3.3.10",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz",
|
||||
"integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.10.tgz",
|
||||
"integrity": "sha512-NCrqF5fm10GXZIK0GrEAauBqdy+F2LZRt3yNHzrYjpYBuRssQbuPLtSnSNjyR9luHKkWSH8we5LMB3g+4z2HvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.3.8",
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/compiler-core": "3.3.10",
|
||||
"@vue/shared": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz",
|
||||
"integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.10.tgz",
|
||||
"integrity": "sha512-xpcTe7Rw7QefOTRFFTlcfzozccvjM40dT45JtrE3onGm/jBLZ0JhpKu3jkV7rbDFLeeagR/5RlJ2Y9SvyS0lAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/compiler-core": "3.3.8",
|
||||
"@vue/compiler-dom": "3.3.8",
|
||||
"@vue/compiler-ssr": "3.3.8",
|
||||
"@vue/reactivity-transform": "3.3.8",
|
||||
"@vue/shared": "3.3.8",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@vue/compiler-core": "3.3.10",
|
||||
"@vue/compiler-dom": "3.3.10",
|
||||
"@vue/compiler-ssr": "3.3.10",
|
||||
"@vue/reactivity-transform": "3.3.10",
|
||||
"@vue/shared": "3.3.10",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.4.32",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz",
|
||||
"integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.10.tgz",
|
||||
"integrity": "sha512-12iM4jA4GEbskwXMmPcskK5wImc2ohKm408+o9iox3tfN9qua8xL0THIZtoe9OJHnXP4eOWZpgCAAThEveNlqQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.8",
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/compiler-dom": "3.3.10",
|
||||
"@vue/shared": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
@@ -598,83 +616,83 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz",
|
||||
"integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.10.tgz",
|
||||
"integrity": "sha512-0xBdk+CKHWT+Gev8oZ63Tc0qFfj935YZx+UAynlutnrDZ4diFCVFMWixn65HzjE3S1iJppWOo6Tt1OzASH7VEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@vue/compiler-core": "3.3.8",
|
||||
"@vue/shared": "3.3.8",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@vue/compiler-core": "3.3.10",
|
||||
"@vue/shared": "3.3.10",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz",
|
||||
"integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.10.tgz",
|
||||
"integrity": "sha512-DZ0v31oTN4YHX9JEU5VW1LoIVgFovWgIVb30bWn9DG9a7oA415idcwsRNNajqTx8HQJyOaWfRKoyuP2P2TYIag==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.3.8",
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/reactivity": "3.3.10",
|
||||
"@vue/shared": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz",
|
||||
"integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.10.tgz",
|
||||
"integrity": "sha512-H5Z7rOY/JLO+e5a6/FEXaQ1TMuOvY4LDVgT+/+HKubEAgs9qeeZ+NhADSeEtrNQeiKLDuzeKc8v0CUFpB6Pqgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/shared": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz",
|
||||
"integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.10.tgz",
|
||||
"integrity": "sha512-c/jKb3ny05KJcYk0j1m7Wbhrxq7mZYr06GhKykDMNRRR9S+/dGT8KpHuNQjv3/8U4JshfkAk6TpecPD3B21Ijw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.3.8",
|
||||
"@vue/shared": "3.3.8",
|
||||
"@vue/runtime-core": "3.3.10",
|
||||
"@vue/shared": "3.3.10",
|
||||
"csstype": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz",
|
||||
"integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.10.tgz",
|
||||
"integrity": "sha512-0i6ww3sBV3SKlF3YTjSVqKQ74xialMbjVYGy7cOTi7Imd8ediE7t72SK3qnvhrTAhOvlQhq6Bk6nFPdXxe0sAg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.3.8",
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/compiler-ssr": "3.3.10",
|
||||
"@vue/shared": "3.3.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.3.8"
|
||||
"vue": "3.3.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
@@ -683,9 +701,9 @@
|
||||
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "3.13.2",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.2.tgz",
|
||||
"integrity": "sha512-WzojeeN082kLZznGI1HAuP8yFJSWqJ1fGdz2mUjj45H4y0XwToE7fFqtI3mCPRR+BpcSbxT/NL+FyPnYAWSltw==",
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz",
|
||||
"integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "~3.1.1"
|
||||
}
|
||||
@@ -896,6 +914,14 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -956,9 +982,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.3.1.tgz",
|
||||
"integrity": "sha512-dCi91VD+57lkoBd10CjdW4wPOeOPYvvzQbxti6xmyQbDMbCeCXwNq2KdoU798I4OsCcD5B+n7yVG7HAgYW+cvw==",
|
||||
"version": "4.4.19",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.4.19.tgz",
|
||||
"integrity": "sha512-IjOLWwnndD4N7Ut5CDxbUsaVtbqXPeVHM92IcgxGFxpuOd3CCKW/PAXZH6JoBTHFRaN57vB9XqEhdWm5yC+bPA==",
|
||||
"dependencies": {
|
||||
"css-selector-tokenizer": "^0.8",
|
||||
"culori": "^3",
|
||||
@@ -973,6 +999,22 @@
|
||||
"url": "https://opencollective.com/daisyui"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -982,6 +1024,14 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -1100,6 +1150,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz",
|
||||
"integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==",
|
||||
"dependencies": {
|
||||
"tabbable": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
@@ -1233,6 +1291,29 @@
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
|
||||
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@@ -1290,6 +1371,15 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-echo": {
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.15.3.tgz",
|
||||
"integrity": "sha512-SRXzccaat6w4qKgZ4/rjFKr3nJfVxB+ly4V0MEJNIF1/TpERNXepo3uk7NnOjBGsiV/np1fl2XitAzW4Sa1s/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz",
|
||||
@@ -1324,6 +1414,16 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
@@ -1398,6 +1498,11 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -1409,9 +1514,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1518,9 +1623,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
|
||||
"version": "8.4.32",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
|
||||
"integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -1536,7 +1641,7 @@
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
@@ -1647,6 +1752,15 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pusher-js": {
|
||||
"version": "8.4.0-rc2",
|
||||
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz",
|
||||
"integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -1685,6 +1799,25 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.2",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
|
||||
@@ -1775,6 +1908,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
|
||||
},
|
||||
"node_modules/sucrase": {
|
||||
"version": "3.32.0",
|
||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz",
|
||||
@@ -1807,10 +1945,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/tabbable": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz",
|
||||
"integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
@@ -1886,6 +2029,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||
@@ -1922,9 +2071,9 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
|
||||
"integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz",
|
||||
"integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
@@ -1987,16 +2136,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz",
|
||||
"integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.10.tgz",
|
||||
"integrity": "sha512-zg6SIXZdTBwiqCw/1p+m04VyHjLfwtjwz8N57sPaBhEex31ND0RYECVOC1YrRwMRmxFf5T1dabl6SGUbMKKuVw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.8",
|
||||
"@vue/compiler-sfc": "3.3.8",
|
||||
"@vue/runtime-dom": "3.3.8",
|
||||
"@vue/server-renderer": "3.3.8",
|
||||
"@vue/shared": "3.3.8"
|
||||
"@vue/compiler-dom": "3.3.10",
|
||||
"@vue/compiler-sfc": "3.3.10",
|
||||
"@vue/runtime-dom": "3.3.10",
|
||||
"@vue/server-renderer": "3.3.10",
|
||||
"@vue/shared": "3.3.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -2008,9 +2157,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue/node_modules/@vue/shared": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
|
||||
"integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
|
||||
"version": "3.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.10.tgz",
|
||||
"integrity": "sha512-2y3Y2J1a3RhFa0WisHvACJR2ncvWiVHcP8t0Inxo+NKz+8RKO4ZV8eZgCxRgQoA6ITfV12L4E6POOL9HOU5nqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
|
||||
18
package.json
18
package.json
@@ -6,19 +6,23 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "4.5.0",
|
||||
"@vitejs/plugin-vue": "4.5.1",
|
||||
"autoprefixer": "10.4.16",
|
||||
"axios": "1.6.2",
|
||||
"laravel-echo": "1.15.3",
|
||||
"laravel-vite-plugin": "0.8.1",
|
||||
"postcss": "8.4.31",
|
||||
"tailwindcss": "3.3.5",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.8"
|
||||
"postcss": "8.4.32",
|
||||
"pusher-js": "8.4.0-rc2",
|
||||
"tailwindcss": "3.3.6",
|
||||
"vite": "4.5.1",
|
||||
"vue": "3.3.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alpinejs/focus": "^3.13.3",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"alpinejs": "3.13.2",
|
||||
"daisyui": "4.3.1",
|
||||
"alpinejs": "3.13.3",
|
||||
"daisyui": "4.4.19",
|
||||
"ioredis": "5.3.2",
|
||||
"tailwindcss-scrollbar": "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
2
public/js/echo.js
Normal file
2
public/js/echo.js
Normal file
File diff suppressed because one or more lines are too long
10
public/js/pusher.js
Normal file
10
public/js/pusher.js
Normal file
File diff suppressed because one or more lines are too long
2
public/vendor/horizon/app.js
vendored
2
public/vendor/horizon/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/vendor/horizon/mix-manifest.json
vendored
2
public/vendor/horizon/mix-manifest.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"/app.js": "/app.js?id=ff1533ec4a7afad65c5bd7bcc2cc7d7b",
|
||||
"/app.js": "/app.js?id=79bae40dcb18de9ca1b5d0008c577471",
|
||||
"/app-dark.css": "/app-dark.css?id=15c72df05e2b1147fa3e4b0670cfb435",
|
||||
"/app.css": "/app.css?id=4d6a1a7fe095eedc2cb2a4ce822ea8a5",
|
||||
"/img/favicon.png": "/img/favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
|
||||
|
||||
@@ -17,6 +17,9 @@ button[isError] {
|
||||
.main {
|
||||
@apply pt-4 pl-24 pr-10 mx-auto;
|
||||
}
|
||||
.custom-modal {
|
||||
@apply flex flex-col gap-2 px-8 py-4 border bg-base-100 border-coolgray-200;
|
||||
}
|
||||
.label-text,
|
||||
label {
|
||||
@apply text-neutral-400;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Alpine from "alpinejs";
|
||||
|
||||
import focus from '@alpinejs/focus';
|
||||
import { createApp } from "vue";
|
||||
import MagicBar from "./components/MagicBar.vue";
|
||||
import Toaster from "../../vendor/masmerise/livewire-toaster/resources/js";
|
||||
import "../../vendor/wire-elements/modal/resources/js/modal";
|
||||
|
||||
Alpine.plugin(focus);
|
||||
Alpine.plugin(Toaster);
|
||||
|
||||
window.Alpine = Alpine;
|
||||
@@ -12,3 +14,6 @@ Alpine.start();
|
||||
const app = createApp({});
|
||||
app.component("magic-bar", MagicBar);
|
||||
app.mount("#vue");
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,44 +1,50 @@
|
||||
<div class="group">
|
||||
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
|
||||
<x-chevron-down />
|
||||
</label>
|
||||
@if (data_get($application, 'fqdn') ||
|
||||
collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
|
||||
data_get($application, 'previews', collect([]))->count() > 0 ||
|
||||
data_get($application, 'ports_mappings_array'))
|
||||
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
|
||||
<x-chevron-down />
|
||||
</label>
|
||||
|
||||
<div class="absolute z-50 hidden group-hover:block">
|
||||
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
|
||||
@if (data_get($application, 'gitBrancLocation'))
|
||||
<li>
|
||||
<a target="_blank"
|
||||
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
href="{{ $application->gitBranchLocation }}">
|
||||
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
|
||||
Git Repository
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if (data_get($application, 'fqdn'))
|
||||
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
|
||||
<div class="absolute z-50 hidden group-hover:block">
|
||||
<ul tabindex="0"
|
||||
class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
|
||||
@if (data_get($application, 'gitBrancLocation'))
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>{{ getFqdnWithoutPort($fqdn) }}
|
||||
<a target="_blank"
|
||||
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
href="{{ $application->gitBranchLocation }}">
|
||||
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
|
||||
Git Repository
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'previews', collect([]))->count() > 0)
|
||||
@foreach (data_get($application, 'previews') as $preview)
|
||||
@if (data_get($preview, 'fqdn'))
|
||||
@endif
|
||||
@if (data_get($application, 'build_pack') === 'dockercompose')
|
||||
@foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
|
||||
@if (data_get($fqdn, 'domain'))
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'fqdn'))
|
||||
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
|
||||
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
@@ -47,52 +53,72 @@
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
PR{{ data_get($preview, 'pull_request_id') }} |
|
||||
{{ data_get($preview, 'fqdn') }}
|
||||
</svg>{{ getFqdnWithoutPort($fqdn) }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'ports_mappings_array'))
|
||||
@foreach ($application->ports_mappings_array as $port)
|
||||
@if (isDev())
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
Port {{ $port }}
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank"
|
||||
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
Port {{ $port }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'previews', collect([]))->count() > 0)
|
||||
@foreach (data_get($application, 'previews') as $preview)
|
||||
@if (data_get($preview, 'fqdn'))
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
PR{{ data_get($preview, 'pull_request_id') }} |
|
||||
{{ data_get($preview, 'fqdn') }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@if (data_get($application, 'ports_mappings_array'))
|
||||
@foreach ($application->ports_mappings_array as $port)
|
||||
@if (isDev())
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
Port {{ $port }}
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
<li>
|
||||
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
|
||||
target="_blank"
|
||||
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 15l6 -6" />
|
||||
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
|
||||
<path
|
||||
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
|
||||
</svg>
|
||||
Port {{ $port }}
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user