diff --git a/.tinkerwell/snippets/DeleteUser.php b/.tinkerwell/snippets/DeleteUser.php
new file mode 100644
index 000000000..bd562557b
--- /dev/null
+++ b/.tinkerwell/snippets/DeleteUser.php
@@ -0,0 +1,22 @@
+first();
+$teams = $user->teams;
+foreach ($teams as $team) {
+ $servers = $team->servers;
+ if ($servers->count() > 0) {
+ foreach ($servers as $server) {
+ dump($server);
+ $server->delete();
+ }
+ }
+ dump($team);
+ $team->delete();
+}
+if ($user) {
+ dump($user);
+ $user->delete();
+}
diff --git a/app/Http/Livewire/Project/Service/StackForm.php b/app/Http/Livewire/Project/Service/StackForm.php
index 30a3e7380..ebdb2d481 100644
--- a/app/Http/Livewire/Project/Service/StackForm.php
+++ b/app/Http/Livewire/Project/Service/StackForm.php
@@ -7,17 +7,34 @@ use Livewire\Component;
class StackForm extends Component
{
public $service;
- public $isConfigurationRequired = false;
+ public $fields = [];
protected $listeners = ["saveCompose"];
- protected $rules = [
+ public $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
- public function mount () {
- if ($this->service->applications->filter(fn($app) => str($app->image)->contains('minio/minio'))->count() > 0) {
- $this->isConfigurationRequired = true;
+ public $validationAttributes = [];
+ public function mount()
+ {
+ $extraFields = $this->service->extraFields();
+ foreach ($extraFields as $serviceName => $fields) {
+ foreach ($fields as $fieldKey => $field) {
+ $key = data_get($field, 'key');
+ $value = data_get($field, 'value');
+ $rules = data_get($field, 'rules');
+ $isPassword = data_get($field, 'isPassword');
+ $this->fields[$key] = [
+ "serviceName" => $serviceName,
+ "key" => $key,
+ "name" => $fieldKey,
+ "value" => $value,
+ "isPassword" => $isPassword,
+ ];
+ $this->rules["fields.$key.value"] = $rules;
+ $this->validationAttributes["fields.$key.value"] = $fieldKey;
+ }
}
}
public function saveCompose($raw)
@@ -32,6 +49,7 @@ class StackForm extends Component
try {
$this->validate();
$this->service->save();
+ $this->service->saveExtraFields($this->fields);
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 71cf9026d..de32ad1fb 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -934,7 +934,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
return implode(' ', $generated_healthchecks_commands);
}
+ private function pull_latest_image($image)
+ {
+ $this->execute_remote_command(
+ ["echo -n 'Pulling latest image ($image) from the registry.'"],
+ [
+ executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
+ ]
+ );
+ }
private function build_image()
{
if ($this->application->build_pack === 'static') {
@@ -948,6 +957,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
+ if ($this->application->static_image) {
+ $this->pull_latest_image($this->application->static_image);
+ }
if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
@@ -1012,8 +1024,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]
);
} else {
+ // Pure Dockerfile based deployment
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}
@@ -1049,6 +1062,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file()
{
+ if (
+ !$this->application->dockerfile &&
+ (
+ $this->application->build_pack === 'dockerimage' ||
+ $this->application->build_pack === 'dockerfile')
+ ) {
+ $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],
+ );
+ }
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 14084f282..eabdc1d88 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -45,7 +45,168 @@ class Service extends BaseModel
{
return 'service';
}
+ public function extraFields()
+ {
+ $fields = collect([]);
+ $applications = $this->applications()->get();
+ foreach ($applications as $application) {
+ $image = str($application->image)->before(':')->value();
+ switch ($image) {
+ case str($image)->contains('minio'):
+ $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
+ $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
+ $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
+ $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
+ $fields->put('MinIO', [
+ 'Console URL' => [
+ 'key' => data_get($console_url, 'key'),
+ 'value' => data_get($console_url, 'value'),
+ 'rules' => 'required|url',
+ ],
+ 'S3 API URL' => [
+ 'key' => data_get($s3_api_url, 'key'),
+ 'value' => data_get($s3_api_url, 'value'),
+ 'rules' => 'required|url',
+ ],
+ 'Admin User' => [
+ 'key' => data_get($admin_user, 'key'),
+ 'value' => data_get($admin_user, 'value'),
+ 'rules' => 'required',
+ ],
+ 'Admin Password' => [
+ 'key' => data_get($admin_password, 'key'),
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ break;
+ }
+ }
+ $databases = $this->databases()->get();
+ foreach ($databases as $database) {
+ $image = str($database->image)->before(':')->value();
+ switch ($image) {
+ case str($image)->contains('postgres'):
+ $userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
+ $passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
+ $dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
+ $postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
+ $postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
+ $postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
+ $fields->put('PostgreSQL', [
+ 'User' => [
+ 'key' => data_get($postgres_user, 'key'),
+ 'value' => data_get($postgres_user, 'value'),
+ 'rules' => 'required',
+ ],
+ 'Password' => [
+ 'key' => data_get($postgres_password, 'key'),
+ 'value' => data_get($postgres_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ 'Database Name' => [
+ 'key' => data_get($postgres_db_name, 'key'),
+ 'value' => data_get($postgres_db_name, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ break;
+ case str($image)->contains('mysql'):
+ $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
+ $dbNameVariables = ['MYSQL_DATABASE'];
+ $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
+ $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
+ $mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
+ $mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
+ $fields->put('MySQL', [
+ 'User' => [
+ 'key' => data_get($mysql_user, 'key'),
+ 'value' => data_get($mysql_user, 'value'),
+ 'rules' => 'required',
+ ],
+ 'Password' => [
+ 'key' => data_get($mysql_password, 'key'),
+ 'value' => data_get($mysql_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ 'Root Password' => [
+ 'key' => data_get($mysql_root_password, 'key'),
+ 'value' => data_get($mysql_root_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ 'Database Name' => [
+ 'key' => data_get($mysql_db_name, 'key'),
+ 'value' => data_get($mysql_db_name, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ break;
+ case str($image)->contains('mariadb'):
+ $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
+ $passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
+ $dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
+ $mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
+ $mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
+ $mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
+ $mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
+ $fields->put('MariaDB', [
+ 'User' => [
+ 'key' => data_get($mariadb_user, 'key'),
+ 'value' => data_get($mariadb_user, 'value'),
+ 'rules' => 'required',
+ ],
+ 'Password' => [
+ 'key' => data_get($mariadb_password, 'key'),
+ 'value' => data_get($mariadb_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ 'Root Password' => [
+ 'key' => data_get($mariadb_root_password, 'key'),
+ 'value' => data_get($mariadb_root_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ 'Database Name' => [
+ 'key' => data_get($mariadb_db_name, 'key'),
+ 'value' => data_get($mariadb_db_name, 'value'),
+ 'rules' => data_get($mariadb_db_name, 'value') && 'required',
+ ],
+ ]);
+
+ break;
+ }
+ }
+ return $fields;
+ }
+ public function saveExtraFields($fields)
+ {
+ foreach ($fields as $field) {
+ $key = data_get($field, 'key');
+ $value = data_get($field, 'value');
+ $found = $this->environment_variables()->where('key', $key)->first();
+ if ($found) {
+ $found->value = $value;
+ $found->save();
+ } else {
+ $this->environment_variables()->create([
+ 'key' => $key,
+ 'value' => $value,
+ 'is_build_time' => false,
+ 'service_id' => $this->id,
+ 'is_preview' => false,
+ ]);
+ }
+ }
+ }
public function documentation()
{
$services = getServiceTemplates();
@@ -257,7 +418,7 @@ class Service extends BaseModel
}
}
$networks = collect();
- foreach ($serviceNetworks as $key =>$serviceNetwork) {
+ foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
@@ -268,7 +429,7 @@ class Service extends BaseModel
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
- $networks->put($key,$serviceNetwork);
+ $networks->put($key, $serviceNetwork);
}
}
foreach ($definedNetwork as $key => $network) {
@@ -564,12 +725,18 @@ class Service extends BaseModel
}
// Add labels to the service
- $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 (!$isDatabase) {
+ 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 ($fqdns->count() > 0) {
+ if ($fqdns) {
+ $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
+ }
}
}
data_set($service, 'labels', $serviceLabels->toArray());
diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php
index c70ceeccf..b1db1c581 100644
--- a/app/Models/ServiceApplication.php
+++ b/app/Models/ServiceApplication.php
@@ -22,6 +22,16 @@ class ServiceApplication extends BaseModel
{
return 'service';
}
+ 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 service()
{
return $this->belongsTo(Service::class);
diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php
index 0dbbf6196..e37821af2 100644
--- a/app/Models/ServiceDatabase.php
+++ b/app/Models/ServiceDatabase.php
@@ -29,7 +29,6 @@ class ServiceDatabase extends BaseModel
return "standalone-$image";
}
public function getServiceDatabaseUrl() {
- // $type = $this->databaseType();
$port = $this->public_port;
$realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) {
diff --git a/app/View/Components/Services/Links.php b/app/View/Components/Services/Links.php
index b2cc8618d..b3953c174 100644
--- a/app/View/Components/Services/Links.php
+++ b/app/View/Components/Services/Links.php
@@ -16,22 +16,28 @@ class Links extends Component
{
$this->links = collect([]);
$service->applications()->get()->map(function ($application) {
- if ($application->fqdn) {
- $fqdns = collect(Str::of($application->fqdn)->explode(','));
- $fqdns->map(function ($fqdn) {
- $this->links->push(getFqdnWithoutPort($fqdn));
- });
- }
- if ($application->ports) {
- $portsCollection = collect(Str::of($application->ports)->explode(','));
- $portsCollection->map(function ($port) {
- if (Str::of($port)->contains(':')) {
- $hostPort = Str::of($port)->before(':');
- } else {
- $hostPort = $port;
- }
- $this->links->push(base_url(withPort:false) . ":{$hostPort}");
- });
+ $type = $application->serviceType();
+ if ($type) {
+ $links = generateServiceSpecificFqdns($application, false);
+ $this->links = $this->links->merge($links);
+ } else {
+ if ($application->fqdn) {
+ $fqdns = collect(Str::of($application->fqdn)->explode(','));
+ $fqdns->map(function ($fqdn) {
+ $this->links->push(getFqdnWithoutPort($fqdn));
+ });
+ }
+ if ($application->ports) {
+ $portsCollection = collect(Str::of($application->ports)->explode(','));
+ $portsCollection->map(function ($port) {
+ if (Str::of($port)->contains(':')) {
+ $hostPort = Str::of($port)->before(':');
+ } else {
+ $hostPort = $port;
+ }
+ $this->links->push(base_url(withPort: false) . ":{$hostPort}");
+ });
+ }
}
});
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index 10c1353d3..e844efea9 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -23,3 +23,6 @@ const DATABASE_DOCKER_IMAGES = [
'influxdb',
'clickhouse/clickhouse-server'
];
+const SPECIFIC_SERVICES = [
+ 'quay.io/minio/minio',
+];
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 90c4179a7..7a92ab072 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -144,6 +144,39 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
}
return $labels;
}
+function generateServiceSpecificFqdns($service, $forTraefik = false)
+{
+ $variables = collect($service->service->environment_variables);
+ $type = $service->serviceType();
+ $payload = collect([]);
+ switch ($type) {
+ case $type->contains('minio'):
+ $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
+ if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
+ $MINIO_BROWSER_REDIRECT_URL->update([
+ "value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
+ ]);
+ }
+ $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
+ if (is_null($MINIO_SERVER_URL?->value)) {
+ $MINIO_SERVER_URL->update([
+ "value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
+ ]);
+ }
+ if ($forTraefik) {
+ $payload = collect([
+ $MINIO_BROWSER_REDIRECT_URL->value . ':9001',
+ $MINIO_SERVER_URL->value . ':9000',
+ ]);
+ } else {
+ $payload = collect([
+ $MINIO_BROWSER_REDIRECT_URL->value,
+ $MINIO_SERVER_URL->value,
+ ]);
+ }
+ }
+ return $payload;
+}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{
$labels = collect([]);
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index b9a305a42..43a15444c 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -126,7 +126,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
function updateCompose($resource)
{
try {
- ray($resource);
$name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
$dockerCompose = Yaml::parse($dockerComposeRaw);
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 013821bd7..d6766a8d0 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -27,7 +27,6 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
-use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA;
@@ -173,7 +172,11 @@ function get_latest_version_of_coolify(): string
function generate_random_name(?string $cuid = null): string
{
- $generator = All::create();
+ $generator = new \Nubs\RandomNameGenerator\All(
+ [
+ new \Nubs\RandomNameGenerator\Alliteration(),
+ ]
+ );
if (is_null($cuid)) {
$cuid = new Cuid2(7);
}
@@ -444,20 +447,25 @@ function getServiceTemplates()
if (isDev()) {
$services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys();
- $version = config('version');
- $services = $services->map(function ($service) use ($version) {
- if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
- $service->disabled = true;
- }
- return $service;
- });
} else {
- $services = Http::get(config('constants.services.official'));
- if ($services->failed()) {
- throw new \Exception($services->body());
+ try {
+ $response = Http::retry(3, 50)->get(config('constants.services.official'));
+ if ($response->failed()) {
+ return collect([]);
+ }
+ $services = $response->json();
+ $services = collect($services)->sortKeys();
+ } catch (\Throwable $e) {
+ $services = collect([]);
}
- $services = collect($services->json())->sortKeys();
}
+ // $version = config('version');
+ // $services = $services->map(function ($service) use ($version) {
+ // if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
+ // $service->disabled = true;
+ // }
+ // return $service;
+ // });
return $services;
}
@@ -493,7 +501,8 @@ function queryResourcesByUuid(string $uuid)
return $resource;
}
-function generateDeployWebhook($resource) {
+function generateDeployWebhook($resource)
+{
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy';
@@ -501,6 +510,7 @@ function generateDeployWebhook($resource) {
$url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url;
}
-function removeAnsiColors($text) {
+function removeAnsiColors($text)
+{
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
}
diff --git a/config/sentry.php b/config/sentry.php
index c889afb12..d52a8a9b4 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.123',
+ 'release' => '4.0.0-beta.124',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/version.php b/config/version.php
index f31b19ca0..352ef7c4b 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
-