Merge pull request #4874 from coollabsio/next

v4.0.0-beta.383
This commit is contained in:
Andras Bacsai
2025-01-20 14:06:01 +01:00
committed by GitHub
15 changed files with 249 additions and 29 deletions

View File

@@ -195,6 +195,31 @@ class SecurityController extends Controller
if (! $request->description) {
$request->offsetSet('description', 'Created by Coolify via API');
}
$isPrivateKeyString = str_starts_with($request->private_key, '-----BEGIN');
if (! $isPrivateKeyString) {
try {
$base64PrivateKey = base64_decode($request->private_key);
$request->offsetSet('private_key', $base64PrivateKey);
} catch (\Exception $e) {
return response()->json([
'message' => 'Invalid private key.',
], 422);
}
}
$isPrivateKeyValid = PrivateKey::validatePrivateKey($request->private_key);
if (! $isPrivateKeyValid) {
return response()->json([
'message' => 'Invalid private key.',
], 422);
}
$fingerPrint = PrivateKey::generateFingerprint($request->private_key);
$isFingerPrintExists = PrivateKey::fingerprintExists($fingerPrint);
if ($isFingerPrintExists) {
return response()->json([
'message' => 'Private key already exists.',
], 422);
}
$key = PrivateKey::create([
'team_id' => $teamId,
'name' => $request->name,

View File

@@ -530,11 +530,11 @@ class ServersController extends Controller
'user' => $request->user,
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]);
$server->proxy->set('type', $proxyType);
$server->proxy->set('status', ProxyStatus::EXITED->value);
$server->save();
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
@@ -742,6 +742,9 @@ class ServersController extends Controller
if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
}
if ($server->isLocalhost()) {
return response()->json(['message' => 'Local server cannot be deleted.'], 400);
}
$server->delete();
DeleteServer::dispatch($server);

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use App\Services\DockerImageParser;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -28,12 +29,10 @@ class DockerImage extends Component
$this->validate([
'dockerImage' => 'required',
]);
$image = str($this->dockerImage)->before(':');
if (str($this->dockerImage)->contains(':')) {
$tag = str($this->dockerImage)->after(':');
} else {
$tag = 'latest';
}
$parser = new DockerImageParser;
$parser->parse($this->dockerImage);
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
@@ -53,8 +52,8 @@ class DockerImage extends Component
'git_branch' => 'main',
'build_pack' => 'dockerimage',
'ports_exposes' => 80,
'docker_registry_image_name' => $image,
'docker_registry_image_tag' => $tag,
'docker_registry_image_name' => $parser->getFullImageNameWithoutTag(),
'docker_registry_image_tag' => $parser->getTag(),
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,

View File

@@ -40,6 +40,8 @@ class PrivateKey extends BaseModel
'private_key' => 'encrypted',
];
protected $appends = ['public_key'];
protected static function booted()
{
static::saving(function ($key) {
@@ -64,6 +66,11 @@ class PrivateKey extends BaseModel
});
}
public function getPublicKeyAttribute()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
}
public function getPublicKey()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
@@ -208,15 +215,14 @@ class PrivateKey extends BaseModel
{
try {
$key = PublicKeyLoader::load($privateKey);
$publicKey = $key->getPublicKey();
return $publicKey->getFingerprint('sha256');
return $key->getPublicKey()->getFingerprint('sha256');
} catch (\Throwable $e) {
return null;
}
}
private static function fingerprintExists($fingerprint, $excludeId = null)
public static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::query()
->where('fingerprint', $fingerprint)

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Services;
class DockerImageParser
{
private string $registryUrl = '';
private string $imageName = '';
private string $tag = 'latest';
public function parse(string $imageString): self
{
// First split by : to handle the tag, but be careful with registry ports
$lastColon = strrpos($imageString, ':');
$hasSlash = str_contains($imageString, '/');
// If the last colon appears after the last slash, it's a tag
// Otherwise it might be a port in the registry URL
if ($lastColon !== false && (! $hasSlash || $lastColon > strrpos($imageString, '/'))) {
$mainPart = substr($imageString, 0, $lastColon);
$this->tag = substr($imageString, $lastColon + 1);
} else {
$mainPart = $imageString;
$this->tag = 'latest';
}
// Split the main part by / to handle registry and image name
$pathParts = explode('/', $mainPart);
// If we have more than one part and the first part contains a dot or colon
// it's likely a registry URL
if (count($pathParts) > 1 && (str_contains($pathParts[0], '.') || str_contains($pathParts[0], ':'))) {
$this->registryUrl = array_shift($pathParts);
$this->imageName = implode('/', $pathParts);
} else {
$this->imageName = $mainPart;
}
return $this;
}
public function getFullImageNameWithoutTag(): string
{
return $this->registryUrl.'/'.$this->imageName;
}
public function getRegistryUrl(): string
{
return $this->registryUrl;
}
public function getImageName(): string
{
return $this->imageName;
}
public function getTag(): string
{
return $this->tag;
}
public function toString(): string
{
$parts = [];
if ($this->registryUrl) {
$parts[] = $this->registryUrl;
}
$parts[] = $this->imageName;
return implode('/', $parts).':'.$this->tag;
}
}

View File

@@ -2872,7 +2872,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
data_set($service, 'environment', $serviceVariables->toArray());
updateCompose($savedService);
updateCompose($service);
return $service;
});

View File

@@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.382',
'version' => '4.0.0-beta.383',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View File

@@ -5,10 +5,10 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
CDN="https://cdn.coollabs.io/coolify"
CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
VERSION="1.7"
DOCKER_VERSION="27.0"
# TODO: Ask for a user
CURRENT_USER=$USER
@@ -488,13 +488,13 @@ fi
# Add default root user credentials from environment variables
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
if ! grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
fi
if ! grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
fi
if ! grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
fi
fi

View File

@@ -31,7 +31,7 @@
</div>
<div class="">
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
<div class="pb-4">Read the docs <a class='dark:text-white'
<div class="pb-4">Read the docs <a class='underline dark:text-white'
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.</div>
@if ($is_swarm_worker || $is_build_server)
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager"

View File

@@ -8,7 +8,7 @@ set -o pipefail # Cause a pipeline to return the status of the last command that
CDN="https://cdn.coollabs.io/coolify"
DATE=$(date +"%Y%m%d-%H%M%S")
VERSION="1.6"
VERSION="1.7"
DOCKER_VERSION="27.0"
# TODO: Ask for a user
CURRENT_USER=$USER
@@ -22,6 +22,11 @@ echo -e "Welcome to Coolify Installer!"
echo -e "This script will install everything for you. Sit back and relax."
echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n"
# Predefined root user
ROOT_USERNAME=${ROOT_USERNAME:-}
ROOT_USER_EMAIL=${ROOT_USER_EMAIL:-}
ROOT_USER_PASSWORD=${ROOT_USER_PASSWORD:-}
TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
REQUIRED_TOTAL_SPACE=30
@@ -481,6 +486,19 @@ else
sed -i "s|^PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|" "$ENV_FILE-$DATE"
fi
# Add default root user credentials from environment variables
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
fi
if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
fi
if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
fi
fi
# Merge .env and .env.production. New values will be added to .env
echo -e "7. Propagating .env with new values - if necessary."
awk -F '=' '!seen[$1]++' "$ENV_FILE-$DATE" /data/coolify/source/.env.production > $ENV_FILE

View File

@@ -33,6 +33,7 @@ fi
docker network create --attachable coolify 2>/dev/null
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
echo "If you encounter any issues, please check the log file: $LOGFILE"
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
echo "docker-compose.custom.yml detected." >> $LOGFILE
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1

View File

@@ -88,7 +88,7 @@ services:
retries: 3
postgres:
image: postgres:12
image: pgvector/pgvector:pg12
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,94 @@
<?php
namespace Tests\Unit\Services;
use App\Services\DockerImageParser;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
class DockerImageParserTest extends TestCase
{
private DockerImageParser $parser;
protected function setUp(): void
{
parent::setUp();
$this->parser = new DockerImageParser;
}
#[Test]
public function it_parses_simple_image_name()
{
$this->parser->parse('nginx');
$this->assertEquals('', $this->parser->getRegistryUrl());
$this->assertEquals('nginx', $this->parser->getImageName());
$this->assertEquals('latest', $this->parser->getTag());
}
#[Test]
public function it_parses_image_with_tag()
{
$this->parser->parse('nginx:1.19');
$this->assertEquals('', $this->parser->getRegistryUrl());
$this->assertEquals('nginx', $this->parser->getImageName());
$this->assertEquals('1.19', $this->parser->getTag());
}
#[Test]
public function it_parses_image_with_organization()
{
$this->parser->parse('coollabs/coolify:latest');
$this->assertEquals('', $this->parser->getRegistryUrl());
$this->assertEquals('coollabs/coolify', $this->parser->getImageName());
$this->assertEquals('latest', $this->parser->getTag());
}
#[Test]
public function it_parses_image_with_registry_url()
{
$this->parser->parse('ghcr.io/coollabs/coolify:v4');
$this->assertEquals('ghcr.io', $this->parser->getRegistryUrl());
$this->assertEquals('coollabs/coolify', $this->parser->getImageName());
$this->assertEquals('v4', $this->parser->getTag());
}
#[Test]
public function it_parses_image_with_port_in_registry()
{
$this->parser->parse('localhost:5000/my-app:dev');
$this->assertEquals('localhost:5000', $this->parser->getRegistryUrl());
$this->assertEquals('my-app', $this->parser->getImageName());
$this->assertEquals('dev', $this->parser->getTag());
}
#[Test]
public function it_parses_image_without_tag()
{
$this->parser->parse('ghcr.io/coollabs/coolify');
$this->assertEquals('ghcr.io', $this->parser->getRegistryUrl());
$this->assertEquals('coollabs/coolify', $this->parser->getImageName());
$this->assertEquals('latest', $this->parser->getTag());
}
#[Test]
public function it_converts_back_to_string()
{
$originalString = 'ghcr.io/coollabs/coolify:v4';
$this->parser->parse($originalString);
$this->assertEquals($originalString, $this->parser->toString());
}
#[Test]
public function it_converts_to_string_with_default_tag()
{
$this->parser->parse('nginx');
$this->assertEquals('nginx:latest', $this->parser->toString());
}
}

View File

@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.382"
"version": "4.0.0-beta.383"
},
"nightly": {
"version": "4.0.0-beta.383"
"version": "4.0.0-beta.384"
},
"helper": {
"version": "1.0.4"