diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php index a14b0da20..fdd46b100 100644 --- a/app/Http/Controllers/Api/SecurityController.php +++ b/app/Http/Controllers/Api/SecurityController.php @@ -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, diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index b1deb5321..a9a0a2e53 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -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); diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 942924437..7d68ce068 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -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, diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 80015d87f..0e702e460 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -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) diff --git a/app/Services/DockerImageParser.php b/app/Services/DockerImageParser.php new file mode 100644 index 000000000..4987f953d --- /dev/null +++ b/app/Services/DockerImageParser.php @@ -0,0 +1,74 @@ + 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; + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 5d34c63b6..ff2933f72 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -2005,7 +2005,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal projectName: $resource->project()->name, resourceName: $resource->name, type: 'service', - subType: $isDatabase ? 'database' : 'application', + subType: $isDatabase ? 'database' : 'application', subId: $savedService->id, subName: $savedService->name, environment: $resource->environment->name, @@ -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; }); diff --git a/config/constants.php b/config/constants.php index 1aff0b787..fc2d92c56 100644 --- a/config/constants.php +++ b/config/constants.php @@ -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'), diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 766b1fc63..31d0a1759 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -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 diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php index b0786341e..225a59a5a 100644 --- a/resources/views/livewire/server/new/by-ip.blade.php +++ b/resources/views/livewire/server/new/by-ip.blade.php @@ -31,7 +31,7 @@

Swarm (experimental)

-
Read the docs Read the docs here.
@if ($is_swarm_worker || $is_build_server) $ENV_FILE diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 1bdf0f461..369ac067d 100644 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -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 diff --git a/templates/compose/chatwoot.yaml b/templates/compose/chatwoot.yaml index aaeb73071..b7eec88cc 100644 --- a/templates/compose/chatwoot.yaml +++ b/templates/compose/chatwoot.yaml @@ -88,7 +88,7 @@ services: retries: 3 postgres: - image: postgres:12 + image: pgvector/pgvector:pg12 restart: always volumes: - postgres-data:/var/lib/postgresql/data diff --git a/templates/service-templates.json b/templates/service-templates.json index 4de2e70e6..c703f9bdc 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -358,7 +358,7 @@ "chatwoot": { "documentation": "https://www.chatwoot.com/docs/self-hosted/?utm_source=coolify.io", "slogan": "Delightful customer relationships at scale.", - "compose": "c2VydmljZXM6CiAgY2hhdHdvb3Q6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUyAtZCBjaGF0d29vdCAtaCAxMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6YWxwaW5lJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBjb21tYW5kOgogICAgICAtIHNoCiAgICAgIC0gJy1jJwogICAgICAtICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAiJFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMiJwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUK", + "compose": "c2VydmljZXM6CiAgY2hhdHdvb3Q6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwZ3ZlY3Rvci9wZ3ZlY3RvcjpwZzEyJwogICAgcmVzdGFydDogYWx3YXlzCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1jaGF0d29vdH0nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkU0VSVklDRV9VU0VSX1BPU1RHUkVTIC1kIGNoYXR3b290IC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gJy1hJwogICAgICAgIC0gJFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", "tags": [ "chatwoot", "chat", diff --git a/tests/Unit/Services/DockerImageParserTest.php b/tests/Unit/Services/DockerImageParserTest.php new file mode 100644 index 000000000..876d68ef0 --- /dev/null +++ b/tests/Unit/Services/DockerImageParserTest.php @@ -0,0 +1,94 @@ +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()); + } +} diff --git a/versions.json b/versions.json index f8214d091..a1a01432c 100644 --- a/versions.json +++ b/versions.json @@ -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"