Merge pull request #1439 from coollabsio/next

v4.0.0-beta.124
This commit is contained in:
Andras Bacsai 2023-11-13 13:03:46 +01:00 committed by GitHub
commit ea64e9d5ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 372 additions and 96 deletions

View File

@ -0,0 +1,22 @@
<?php
use App\Models\User;
$email = 'test@example.com';
$user = User::whereEmail($email)->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();
}

View File

@ -7,17 +7,34 @@
class StackForm extends Component class StackForm extends Component
{ {
public $service; public $service;
public $isConfigurationRequired = false; public $fields = [];
protected $listeners = ["saveCompose"]; protected $listeners = ["saveCompose"];
protected $rules = [ public $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'nullable', 'service.description' => 'nullable',
]; ];
public function mount () { public $validationAttributes = [];
if ($this->service->applications->filter(fn($app) => str($app->image)->contains('minio/minio'))->count() > 0) { public function mount()
$this->isConfigurationRequired = true; {
$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) public function saveCompose($raw)
@ -32,6 +49,7 @@ public function submit()
try { try {
$this->validate(); $this->validate();
$this->service->save(); $this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();

View File

@ -934,7 +934,16 @@ private function generate_healthcheck_commands()
} }
return implode(' ', $generated_healthchecks_commands); 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() private function build_image()
{ {
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
@ -948,6 +957,9 @@ private function build_image()
} }
if ($this->application->settings->is_static || $this->application->build_pack === 'static') { 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') { if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/ WORKDIR /usr/share/nginx/html/
@ -1012,8 +1024,9 @@ private function build_image()
] ]
); );
} else { } else {
// Pure Dockerfile based deployment
$this->execute_remote_command([ $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 @@ private function stop_running_container(bool $force = false)
private function start_by_compose_file() 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( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],

View File

@ -45,7 +45,168 @@ public function type()
{ {
return 'service'; 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() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@ -564,14 +725,20 @@ public function parse(bool $isNew = false): Collection
} }
// Add labels to the service // Add labels to the service
if (!$isDatabase) {
if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns')); $fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if ($fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
}
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE); data_set($service, 'restart', RESTART_MODE);

View File

@ -22,6 +22,16 @@ public function type()
{ {
return 'service'; 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() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);

View File

@ -29,7 +29,6 @@ public function databaseType()
return "standalone-$image"; return "standalone-$image";
} }
public function getServiceDatabaseUrl() { public function getServiceDatabaseUrl() {
// $type = $this->databaseType();
$port = $this->public_port; $port = $this->public_port;
$realIp = $this->service->server->ip; $realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) { if ($realIp === 'host.docker.internal' || isDev()) {

View File

@ -16,6 +16,11 @@ public function __construct(public Service $service)
{ {
$this->links = collect([]); $this->links = collect([]);
$service->applications()->get()->map(function ($application) { $service->applications()->get()->map(function ($application) {
$type = $application->serviceType();
if ($type) {
$links = generateServiceSpecificFqdns($application, false);
$this->links = $this->links->merge($links);
} else {
if ($application->fqdn) { if ($application->fqdn) {
$fqdns = collect(Str::of($application->fqdn)->explode(',')); $fqdns = collect(Str::of($application->fqdn)->explode(','));
$fqdns->map(function ($fqdn) { $fqdns->map(function ($fqdn) {
@ -33,6 +38,7 @@ public function __construct(public Service $service)
$this->links->push(base_url(withPort: false) . ":{$hostPort}"); $this->links->push(base_url(withPort: false) . ":{$hostPort}");
}); });
} }
}
}); });
} }

View File

@ -23,3 +23,6 @@
'influxdb', 'influxdb',
'clickhouse/clickhouse-server' 'clickhouse/clickhouse-server'
]; ];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];

View File

@ -144,6 +144,39 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; 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) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);

View File

@ -126,7 +126,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
function updateCompose($resource) function updateCompose($resource)
{ {
try { try {
ray($resource);
$name = data_get($resource, 'name'); $name = data_get($resource, 'name');
$dockerComposeRaw = data_get($resource, 'service.docker_compose_raw'); $dockerComposeRaw = data_get($resource, 'service.docker_compose_raw');
$dockerCompose = Yaml::parse($dockerComposeRaw); $dockerCompose = Yaml::parse($dockerComposeRaw);

View File

@ -27,7 +27,6 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
@ -173,7 +172,11 @@ function get_latest_version_of_coolify(): string
function generate_random_name(?string $cuid = null): 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)) { if (is_null($cuid)) {
$cuid = new Cuid2(7); $cuid = new Cuid2(7);
} }
@ -444,20 +447,25 @@ function getServiceTemplates()
if (isDev()) { if (isDev()) {
$services = File::get(base_path('templates/service-templates.json')); $services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys(); $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 { } else {
$services = Http::get(config('constants.services.official')); try {
if ($services->failed()) { $response = Http::retry(3, 50)->get(config('constants.services.official'));
throw new \Exception($services->body()); if ($response->failed()) {
return collect([]);
} }
$services = collect($services->json())->sortKeys(); $services = $response->json();
$services = collect($services)->sortKeys();
} catch (\Throwable $e) {
$services = collect([]);
} }
}
// $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; return $services;
} }
@ -493,7 +501,8 @@ function queryResourcesByUuid(string $uuid)
return $resource; return $resource;
} }
function generateDeployWebhook($resource) { function generateDeployWebhook($resource)
{
$baseUrl = base_url(); $baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1'; $api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy'; $endpoint = '/deploy';
@ -501,6 +510,7 @@ function generateDeployWebhook($resource) {
$url = $api . $endpoint . "?uuid=$uuid&force=false"; $url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url; return $url;
} }
function removeAnsiColors($text) { function removeAnsiColors($text)
{
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
} }

View File

@ -7,7 +7,7 @@
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // 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 // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.123'; return '4.0.0-beta.124';

View File

@ -44,6 +44,7 @@ services:
- STRIPE_PRICE_ID_PRO_YEARLY - STRIPE_PRICE_ID_PRO_YEARLY
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY - STRIPE_PRICE_ID_ULTIMATE_MONTHLY
- STRIPE_PRICE_ID_ULTIMATE_YEARLY - STRIPE_PRICE_ID_ULTIMATE_YEARLY
- STRIPE_EXCLUDED_PLANS
- PADDLE_VENDOR_ID - PADDLE_VENDOR_ID
- PADDLE_WEBHOOK_SECRET - PADDLE_WEBHOOK_SECRET
- PADDLE_VENDOR_AUTH_CODE - PADDLE_VENDOR_AUTH_CODE

View File

@ -3,7 +3,7 @@
href="{{ route('project.service', $parameters) }}"> href="{{ route('project.service', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<x-services.links :service="$service" /> <x-services.links />
<div class="flex-1"></div> <div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded') @if (serviceStatus($service) === 'degraded')
<button wire:click='deploy' onclick="startService.showModal()" <button wire:click='deploy' onclick="startService.showModal()"

View File

@ -5,7 +5,7 @@
<div class="fixed z-50 top-[4.5rem] left-4" id="vue"> <div class="fixed z-50 top-[4.5rem] left-4" id="vue">
<magic-bar></magic-bar> <magic-bar></magic-bar>
</div> </div>
<main class="main max-w-screen-2xl"> <main class="pb-10 main max-w-screen-2xl">
{{ $slot }} {{ $slot }}
</main> </main>
@endsection @endsection

View File

@ -67,7 +67,7 @@
services, called resources. Any CPU intensive process will use the server's CPU where you services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p> are deploying your resources.</p>
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server <p>Localhost is the server where Coolify is running on. It is not recommended to use one server
for everyting.</p> for everything.</p>
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud <p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
provider.</p> provider.</p>
</x-slot:explanation> </x-slot:explanation>

View File

@ -182,7 +182,7 @@ class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-cool
</button> </button>
@endif @endif
@empty @empty
<div>No service found.</div> <div>No service found. Please try to reload the list!</div>
@endforelse @endforelse
@endif @endif
</div> </div>

View File

@ -16,19 +16,21 @@
<x-forms.input label="Description" id="application.description"></x-forms.input> <x-forms.input label="Description" id="application.description"></x-forms.input>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
@if ($application->required_fqdn) @if ($application->required_fqdn)
<x-forms.input required placeholder="https://app.coolify.io" label="Domains" <x-forms.input required placeholder="https://app.coolify.io" label="Domains"
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> id="application.fqdn"
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@else @else
<x-forms.input placeholder="https://app.coolify.io" label="Domains" <x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input> helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
@endif
@endif @endif
<x-forms.input required <x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image" id="application.image"></x-forms.input> label="Image" id="application.image"></x-forms.input>
</div> </div>
</div> </div>
<h3 class="pt-2">Advanced</h3> <h3 class="pt-2">Advanced</h3>
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status" <x-forms.checkbox instantSave label="Exclude from service status"

View File

@ -28,7 +28,7 @@
<div class="w-full pl-8"> <div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<livewire:project.service.stack-form :service="$service" /> <livewire:project.service.stack-form :service="$service" />
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
@foreach ($applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
'border-l border-dashed border-red-500' => Str::of( 'border-l border-dashed border-red-500' => Str::of(
@ -58,7 +58,7 @@
@endif @endif
<div class="text-xs">{{ $application->status }}</div> <div class="text-xs">{{ $application->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>
@ -88,7 +88,7 @@ class="hover:text-warning">Logs</span></a>
@endif @endif
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</a> </a>
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white" <a class="flex items-center gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
class="hover:text-warning">Logs</span></a> class="hover:text-warning">Logs</span></a>
</div> </div>

View File

@ -12,9 +12,17 @@
<x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" /> <x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" />
<x-forms.input id="service.description" label="Description" /> <x-forms.input id="service.description" label="Description" />
</div> </div>
{{-- @if ($isConfigurationRequired) @if ($fields)
<div class="text-warning">This service requires additional confiugration. Please check our <a <div>
href="https://coolify.io/docs" class="text-white underline">documentation</a> for further information. <h3>Service Specific Configuration</h3>
</div> </div>
@endif --}} <div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $fields)
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
helper="Variable name: {{ $serviceName }}"
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input>
@endforeach
</div>
@endif
</form> </form>

View File

@ -1,4 +1,4 @@
# documentation: https://fider.io/doc # documentation: https://fider.io/docs
# slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services. # slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.
# tags: feedback, user-feedback # tags: feedback, user-feedback

View File

@ -13,29 +13,3 @@ services:
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
volumes: volumes:
- minio-data:/data - minio-data:/data
# services:
# minio:
# image: minio/minio
# command: server /data --address ":9000" --console-address ":9001"
# networks:
# - coolify
# environment:
# - MINIO_SERVER_URL=http://minio.65.21.189.27.sslip.io
# - MINIO_BROWSER_REDIRECT_URL=http://console.65.21.189.27.sslip.io
# - MINIO_BROWSER=on
# - MINIO_ROOT_USER=asd
# - MINIO_ROOT_PASSWORD=asdasdasd
# labels:
# - "traefik.enable=true"
# - "traefik.http.routers.minio-console.rule=Host(`console.65.21.189.27.sslip.io`)"
# - "traefik.http.routers.minio-console.entrypoints=http"
# - "traefik.http.routers.minio-console.service=minio-console"
# - "traefik.http.services.minio-console.loadbalancer.server.port=9001"
# - "traefik.http.routers.minio.rule=Host(`minio.65.21.189.27.sslip.io`)"
# - "traefik.http.routers.minio.entrypoints=http"
# - "traefik.http.routers.minio.service=minio"
# - "traefik.http.services.minio.loadbalancer.server.port=9000"
# networks:
# coolify:
# external: true

View File

@ -132,7 +132,7 @@
] ]
}, },
"fider": { "fider": {
"documentation": "https:\/\/fider.io\/doc", "documentation": "https:\/\/fider.io\/docs",
"slogan": "Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.", "slogan": "Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.",
"compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=", "compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=",
"tags": [ "tags": [

View File

@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.123" "version": "4.0.0-beta.124"
} }
} }
} }