Compare commits

...

47 Commits

Author SHA1 Message Date
Andras Bacsai
ca6db9c1a9 Merge pull request #1440 from coollabsio/next
v4.0.0-beta.125
2023-11-13 13:21:00 +01:00
Andras Bacsai
f27e00e80e Update version.json to include v4.0.0-beta.125 2023-11-13 13:20:28 +01:00
Andras Bacsai
60cf296f31 Update preview application deployment labels and version 2023-11-13 13:20:12 +01:00
Andras Bacsai
ea64e9d5ad Merge pull request #1439 from coollabsio/next
v4.0.0-beta.124
2023-11-13 13:03:46 +01:00
Andras Bacsai
55846c5635 Fix service retrieval and add error handling 2023-11-13 12:59:59 +01:00
Andras Bacsai
7763594e6e Add pull_latest_image function and update
build_image function to use it. Also add check for
dockerfile existence in start_by_compose_file
function.
2023-11-13 12:30:25 +01:00
Andras Bacsai
6b5339c1c1 Remove ray debug statement and refactor random
name generator
2023-11-13 11:44:13 +01:00
Andras Bacsai
f2980738e4 Fix documentation link in service-templates.json 2023-11-13 11:30:20 +01:00
Andras Bacsai
f0e3ad0461 Merge pull request #1432 from AlejandroAkbal/main
fix(fider template): use the correct docs url
2023-11-13 11:29:39 +01:00
Andras Bacsai
187050e098 Merge pull request #1435 from AshikNesin/main
Fix typo in onboarding page
2023-11-13 11:29:02 +01:00
Andras Bacsai
9e7823795d Fix null check for MINIO_BROWSER_REDIRECT_URL and
MINIO_SERVER_URL in generateServiceSpecificFqdns
function
2023-11-13 11:17:49 +01:00
Andras Bacsai
239459dfa8 Remove commented out code for minio service 2023-11-13 11:13:16 +01:00
Andras Bacsai
ce0f560c44 Add service-specific configuration fields and save
them to the database
2023-11-13 11:09:21 +01:00
Andras Bacsai
95baec99dd Fix typo in General.php component 2023-11-13 09:04:19 +01:00
Andras Bacsai
363e8fc0b5 Update code with bug fixes and improvements 2023-11-13 08:46:43 +01:00
Andras Bacsai
e49caba920 Add STRIPE_EXCLUDED_PLANS to services in
docker-compose.prod.yml
2023-11-13 08:46:17 +01:00
Ashik Nesin
30db2b2a09 Update typo in onboarding screen 2023-11-12 19:30:20 +00:00
Andras Bacsai
285666e181 Merge pull request #1434 from coollabsio/next
v4.0.0-beta.123
2023-11-12 19:11:31 +01:00
Andras Bacsai
003934ee1d disable service confs for now 2023-11-12 19:10:54 +01:00
Andras Bacsai
44c7958aa6 make fqdn super long 2023-11-12 19:09:38 +01:00
Alejandro Akbal
35b1a81dfe fix(fider template): use the correct docs url 2023-11-12 12:10:53 +00:00
Andras Bacsai
e40f397cc7 fix: service updates 2023-11-11 21:32:41 +01:00
Andras Bacsai
9fd8cd7e6c Merge pull request #1430 from coollabsio/next
v4.0.0-beta.122
2023-11-11 10:19:28 +01:00
Andras Bacsai
a94b7ee611 fix: container status jobs for old pr deployments 2023-11-11 10:18:40 +01:00
Andras Bacsai
fc68bf50b5 save 2023-11-10 22:04:04 +01:00
Andras Bacsai
0f99ee787c Merge pull request #1429 from coollabsio/next
v4.0.0-beta.121
2023-11-10 21:30:49 +01:00
Andras Bacsai
95777e978e fix: revert workdir to basedir 2023-11-10 21:02:39 +01:00
Andras Bacsai
fb0b9dbfed Add subscription exclusion for certain plans in
webhook handling
2023-11-10 15:41:44 +01:00
Andras Bacsai
9617000daa Add stripe_excluded_plans config variable and
handle excluded plans in webhook
2023-11-10 15:36:02 +01:00
Andras Bacsai
1818404172 Refactor application configuration blade file to
conditionally display tabs based on build pack
2023-11-10 13:46:14 +01:00
Andras Bacsai
d9a966fd98 Fix broken link to framework specific docs in
general.blade.php
2023-11-10 13:42:17 +01:00
Andras Bacsai
763ce5fc14 Update version numbers and deployment logs styling 2023-11-10 13:38:29 +01:00
Andras Bacsai
df021760a7 Merge pull request #1423 from coollabsio/next
v4.0.0-beta.120
2023-11-10 12:06:55 +01:00
Andras Bacsai
fb2598f2e4 Update UI elements and add new build pack option (static) 2023-11-10 11:33:15 +01:00
Andras Bacsai
7af07b2718 Add logging to DockerCleanupJob 2023-11-10 10:55:23 +01:00
Andras Bacsai
23a94c9378 Refactor DockerCleanupJob and Application model 2023-11-10 10:34:28 +01:00
Andras Bacsai
ed34fc9645 Update defaultClass in Select component 2023-11-10 10:14:46 +01:00
Andras Bacsai
cafd9e0ab2 Convert cpus limits to integer in database and
application classes
2023-11-10 09:54:40 +01:00
Andras Bacsai
e882477e21 Refactor navbar and add help us link 2023-11-10 09:49:47 +01:00
Andras Bacsai
db0e3cfcc4 fix: database proxy for services
version++
tiny css modifications
2023-11-10 09:41:42 +01:00
Andras Bacsai
b3c4429028 Merge pull request #1422 from coollabsio/next
v4.0.0-beta.119
2023-11-09 15:10:56 +01:00
Andras Bacsai
87ab4bd71e fix: local ip address 2023-11-09 15:05:42 +01:00
Andras Bacsai
61e1fdede9 feat: make service databases public 2023-11-09 14:59:38 +01:00
Andras Bacsai
b189919f97 Merge pull request #1421 from coollabsio/next
v4.0.0-beta.118
2023-11-09 12:47:05 +01:00
Andras Bacsai
8f5b084931 Refactor environment variable saving logic. 2023-11-09 12:40:53 +01:00
Andras Bacsai
eb96a5ae7b Update user authentication logic to use bcrypt
hashing algorithm
2023-11-09 12:29:03 +01:00
Andras Bacsai
f0fb9dbb94 Update Sentry and version configs to
4.0.0-beta.118
2023-11-09 12:19:08 +01:00
60 changed files with 1194 additions and 580 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

@@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -14,21 +15,53 @@ class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{
$internalPort = null;
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
$type = $database->getMorphClass();
$network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$databaseType = $database->databaseType();
$network = data_get($database, 'service.destination.network');
$server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) {
case 'standalone-mariadb':
$type = 'App\Models\StandaloneMariadb';
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = 'App\Models\StandaloneMongodb';
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = 'App\Models\StandaloneMysql';
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = 'App\Models\StandalonePostgresql';
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = 'App\Models\StandaloneRedis';
$containerName = "redis-{$database->service->uuid}";
break;
}
}
if ($type === 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') {
} else if ($type === 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') {
} else if ($type === 'App\Models\StandaloneMongodb') {
$internalPort = 27017;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMysql') {
} else if ($type === 'App\Models\StandaloneMysql') {
$internalPort = 3306;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMariadb') {
} else if ($type === 'App\Models\StandaloneMariadb') {
$internalPort = 3306;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
@@ -42,7 +75,7 @@ class StartDatabaseProxy
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:$internalPort;
proxy_pass $containerName:$internalPort;
}
}
EOF;
@@ -54,19 +87,19 @@ class StartDatabaseProxy
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
$proxyContainerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'container_name' => $proxyContainerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
$network,
],
'healthcheck' => [
'test' => [
@@ -81,9 +114,9 @@ class StartDatabaseProxy
]
],
'networks' => [
$database->destination->network => [
$network => [
'external' => true,
'name' => $database->destination->network,
'name' => $network,
'attachable' => true,
]
]
@@ -97,6 +130,6 @@ class StartDatabaseProxy
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up --build -d",
], $database->destination->server);
], $server);
}
}

View File

@@ -56,7 +56,7 @@ class StartMariadb
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -63,7 +63,7 @@ class StartMongodb
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -56,7 +56,7 @@ class StartMysql
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -66,7 +66,7 @@ class StartPostgresql
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -65,7 +65,7 @@ class StartRedis
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpus' => (int) $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
@@ -13,9 +14,13 @@ class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$server = data_get($database, 'destination.server');
if ($database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $server);
$database->is_public = false;
$database->save();
}

View File

@@ -2,28 +2,56 @@
namespace App\Http\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\ServiceDatabase;
use Livewire\Component;
class Database extends Component
{
public ServiceDatabase $database;
public ?string $db_url_public = null;
public $fileStorages;
protected $listeners = ["refreshFileStorages"];
protected $rules = [
'database.human_name' => 'nullable',
'database.description' => 'nullable',
'database.image' => 'required',
'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean',
];
public function render()
{
return view('livewire.project.service.database');
}
public function mount() {
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
}
public function instantSave() {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
$this->emit('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
return;
}
StartDatabaseProxy::run($this->database);
$this->db_url_public = $this->database->getServiceDatabaseUrl();
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->db_url_public = null;
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->submit();
}
public function refreshFileStorages()

View File

@@ -7,15 +7,39 @@ use Livewire\Component;
class StackForm extends Component
{
public $service;
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 $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)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
@@ -25,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();

View File

@@ -55,12 +55,17 @@ class All extends Component
{
if ($isPreview) {
$variables = parseEnvFormatToArray($this->variablesPreview);
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
}
foreach ($variables as $key => $variable) {
if ($isPreview) {
$found = $this->resource->environment_variables_preview()->where('key', $key)->first();
} else {
$found = $this->resource->environment_variables()->where('key', $key)->first();
$foundPreview = $this->resource->environment_variables_preview()->where('key', $key)->first();
}
if ($found) {
if ($found->is_shown_once) {
continue;
@@ -68,14 +73,6 @@ class All extends Component
$found->value = $variable;
$found->save();
continue;
}
if ($foundPreview) {
if ($foundPreview->is_shown_once) {
continue;
}
$foundPreview->value = $variable;
$foundPreview->save();
continue;
} else {
$environment = new EnvironmentVariable();
$environment->key = $key;

View File

@@ -119,11 +119,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) {
if (str($this->application->fqdn)->contains(',')) {
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->application->fqdn);
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
}
$template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn);
$host = $url->getHost();
$schema = $url->getScheme();
$random = new Cuid2(7);
@@ -192,6 +197,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else if ($this->application->build_pack === 'static') {
$this->deploy_static_buildpack();
} else {
if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
@@ -227,6 +234,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[
"docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
]
);
$this->execute_remote_command(
[
"docker image prune -f >/dev/null 2>&1",
"hidden" => true,
"ignore_errors" => true,
]
);
}
@@ -421,6 +436,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image();
$this->rolling_update();
}
private function deploy_static_buildpack()
{
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
$this->clone_repository();
$this->cleanup_git();
$this->build_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function rolling_update()
{
@@ -529,7 +561,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"hidden" => true,
],
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
],
);
}
@@ -743,21 +775,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
if ($this->pull_request_id !== 0) {
$newLabels = collect(generateLabelsApplication($this->application, $this->preview));
$newHostLabel = $newLabels->filter(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->reject(function ($label) {
return str($label)->contains('Host');
});
$labels = collect(generateLabelsApplication($this->application, $this->preview));
$labels = $labels->map(function ($label) {
$pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
$replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
$newLabel = preg_replace($pattern, $replacement, $label);
return $newLabel;
});
$labels = $labels->merge($newHostLabel);
// $newHostLabel = $newLabels->filter(function ($label) {
// return str($label)->contains('Host');
// });
// $labels = $labels->reject(function ($label) {
// return str($label)->contains('Host');
// });
// ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
@@ -787,7 +820,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'memswap_limit' => $this->application->limits_memory_swap,
'mem_swappiness' => $this->application->limits_memory_swappiness,
'mem_reservation' => $this->application->limits_memory_reservation,
'cpus' => $this->application->limits_cpus,
'cpus' => (int) $this->application->limits_cpus,
'cpuset' => $this->application->limits_cpuset,
'cpu_shares' => $this->application->limits_cpu_shares,
]
@@ -907,14 +940,57 @@ 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') {
$this->execute_remote_command([
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else {
$this->execute_remote_command([
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]);
}
if ($this->application->settings->is_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') {
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile
COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$nginx_config = base64_encode("server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}");
} else {
$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->build_image_name {$this->workdir}"), "hidden" => true
]);
@@ -941,20 +1017,22 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
root /usr/share/nginx/html;
}
}");
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile-prod")
executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
],
[
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
],
[
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]
);
} 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
]);
}
}
@@ -990,6 +1068,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],

View File

@@ -159,6 +159,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Server;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -10,12 +11,13 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public $timeout = 300;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null;
@@ -33,15 +35,16 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
}
public function handle(): void
{
$queuedCount = 0;
$this->server->applications()->each(function ($application) use ($queuedCount) {
$count = data_get($application->deployments(), 'count', 0);
$queuedCount += $count;
});
if ($queuedCount > 0) {
ray('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping')->color('orange');
$isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) {
$isInprogress = true;
return;
}
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
try {
if (!$this->server->isFunctional()) {
return;
@@ -49,23 +52,25 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerRootFilesystem = "/";
$this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange');
ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server);
instant_remote_process(['docker builder prune -af'], $this->server);
$usageAfter = $this->getFilesystemUsage();
if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name)->color('orange');
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
} else {
ray('DockerCleanupJob failed to save disk space on ' . $this->server->name)->color('orange');
Log::info('DockerCleanupJob failed to save disk space on ' . $this->server->name);
}
} else {
ray('No need to clean up ' . $this->server->name)->color('orange');
ray('No need to clean up ' . $this->server->name);
Log::info('No need to clean up ' . $this->server->name);
}
} catch (\Throwable $e) {
send_internal_notification('DockerCleanupJob failed with: ' . $e->getMessage());
ray($e->getMessage())->color('orange');
ray($e->getMessage());
throw $e;
}
}

View File

@@ -213,6 +213,14 @@ class Application extends BaseModel
return $this->morphTo();
}
public function isDeploymentInprogress() {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
}
return false;
}
public function deployments(int $skip = 0, int $take = 10)
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');

View File

@@ -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) {
@@ -395,6 +556,7 @@ class Service extends BaseModel
$key = Str::of($variableName);
$value = Str::of($variable);
}
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -452,15 +614,31 @@ class Service extends BaseModel
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
// Count _ in $value
$count = substr_count($value->value(), '_');
if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
@@ -476,7 +654,7 @@ class Service extends BaseModel
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN') {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
@@ -547,14 +725,20 @@ class Service extends BaseModel
}
// Add labels to the service
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 (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);

View File

@@ -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);

View File

@@ -28,6 +28,15 @@ class ServiceDatabase extends BaseModel
}
return "standalone-$image";
}
public function getServiceDatabaseUrl() {
$port = $this->public_port;
$realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) {
$realIp = base_ip();
}
$url = "{$realIp}:{$port}";
return $url;
}
public function service()
{
return $this->belongsTo(Service::class);

View File

@@ -58,8 +58,9 @@ class StandaloneRedis extends BaseModel
{
return 'standalone-redis';
}
public function getDbUrl(): string {
if ($this->is_public) {
public function getDbUrl(bool $useInternal = false): string
{
if ($this->is_public && !$useInternal) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";

View File

@@ -20,7 +20,7 @@ class Input extends Component
public bool $readonly = false,
public string|null $helper = null,
public bool $allowToPeak = true,
public string $defaultClass = "input input-sm bg-coolgray-200 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
public string $defaultClass = "input input-sm bg-coolgray-100 rounded text-white w-full disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) {
}

View File

@@ -19,7 +19,7 @@ class Select extends Component
public string|null $label = null,
public string|null $helper = null,
public bool $required = false,
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-200 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
public string $defaultClass = "select select-sm w-full rounded text-white text-sm bg-coolgray-100 font-normal disabled:bg-coolgray-200/50 disabled:border-none"
) {
//
}

View File

@@ -25,7 +25,7 @@ class Textarea extends Component
public bool $readonly = false,
public string|null $helper = null,
public bool $realtimeValidation = false,
public string $defaultClass = "textarea leading-normal bg-coolgray-200 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
public string $defaultClass = "textarea leading-normal bg-coolgray-100 rounded text-white scrollbar disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
) {
//
}

View File

@@ -16,6 +16,11 @@ class Links extends Component
{
$this->links = collect([]);
$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) {
$fqdns = collect(Str::of($application->fqdn)->explode(','));
$fqdns->map(function ($fqdn) {
@@ -30,9 +35,10 @@ class Links extends Component
} else {
$hostPort = $port;
}
$this->links->push(base_url(withPort:false) . ":{$hostPort}");
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
});
}
}
});
}

View File

@@ -23,3 +23,6 @@ const DATABASE_DOCKER_IMAGES = [
'influxdb',
'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;
}
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([]);

View File

@@ -85,7 +85,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
} else {
$fileLocation = $path;
}
ray($path,$fileLocation);
// Exists and is a file
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
// Exists and is a directory
@@ -135,6 +134,7 @@ function updateCompose($resource)
$image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image);
if (!str($resource->fqdn)->contains(',')) {
// Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
@@ -149,6 +149,7 @@ function updateCompose($resource)
$generatedEnv->value = $url;
$generatedEnv->save();
}
}
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw;

View File

@@ -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 = 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;
}
@@ -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);
}

743
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.117',
'release' => '4.0.0-beta.125',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@@ -11,6 +11,7 @@ return [
'stripe_price_id_pro_yearly' => env('STRIPE_PRICE_ID_PRO_YEARLY', null),
'stripe_price_id_ultimate_monthly' => env('STRIPE_PRICE_ID_ULTIMATE_MONTHLY', null),
'stripe_price_id_ultimate_yearly' => env('STRIPE_PRICE_ID_ULTIMATE_YEARLY', null),
'stripe_excluded_plans' => env('STRIPE_EXCLUDED_PLANS', null),
// Paddle

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.117';
return '4.0.0-beta.125';

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->integer('public_port')->nullable();
$table->boolean('is_public')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('public_port');
$table->dropColumn('is_public');
});
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->longText('fqdn')->nullable()->change();
});
Schema::table('application_previews', function (Blueprint $table) {
$table->longText('fqdn')->nullable()->change();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->longText('fqdn')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
Schema::table('application_previews', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
}
};

View File

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

View File

@@ -53,7 +53,7 @@ a {
@apply text-white;
}
.box {
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-200 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
}
.box-without-bg {
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];

View File

@@ -1,7 +1,7 @@
<template>
<Transition name="fade">
<div>
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-200"
<div class="flex items-center p-1 px-2 overflow-hidden transition-all transform rounded cursor-pointer bg-coolgray-100"
@click="showCommandPalette = true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 icon" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">

View File

@@ -1,6 +1,7 @@
@auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-14 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -11,6 +12,18 @@
</svg>
</a>
</li>
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Send us feedback or get help!" class="fixed top-0 right-0 p-2 px-4 pt-4 mt-auto text-xs">
<div class="justify-center" wire:click="help" onclick="help.showModal()">
<svg class="w-5 h-5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,6 +1,7 @@
@auth
<nav class="fixed h-full overflow-hidden overflow-y-auto pt-28 scrollbar">
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 cursor-pointer bg-coolgray-100"><img class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<a href="/" class="fixed top-0 z-50 mx-3 mt-3 bg-transparent cursor-pointer"><img
class="transition rounded w-11 h-11" src="{{ asset('coolify-transparent.png') }}"></a>
<ul class="flex flex-col h-full gap-4 menu flex-nowrap">
<li title="Dashboard">
<a class="hover:bg-transparent" @if (!request()->is('/')) href="/" @endif>
@@ -87,6 +88,18 @@
@if (isInstanceAdmin() && !isCloud())
<livewire:upgrade />
@endif
<li title="Help us!">
<a class="hover:bg-transparent"href="https://coolify.io/sponsorships" target="_blank">
<svg class="icon hover:text-pink-500" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.5 12.572L12 20l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 1 1 7.5 6.572" />
<path
d="M12 6L8.707 9.293a1 1 0 0 0 0 1.414l.543.543c.69.69 1.81.69 2.5 0l1-1a3.182 3.182 0 0 1 4.5 0l2.25 2.25m-7 3l2 2M15 13l2 2" />
</g>
</svg>
</a>
</li>
<li title="Profile">
<a class="hover:bg-transparent" @if (!request()->is('profile')) href="/profile" @endif>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" stroke-width="1.5"

View File

@@ -3,7 +3,7 @@
href="{{ route('project.service', $parameters) }}">
<button>Configuration</button>
</a>
<x-services.links :service="$service" />
<x-services.links />
<div class="flex-1"></div>
@if (serviceStatus($service) === 'degraded')
<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">
<magic-bar></magic-bar>
</div>
<main class="main max-w-screen-2xl">
<main class="pb-10 main max-w-screen-2xl">
{{ $slot }}
</main>
@endsection

View File

@@ -25,7 +25,6 @@
@endif
</head>
@section('body')
<body>
@livewireScripts
<dialog id="help" class="modal">

View File

@@ -67,7 +67,7 @@
services, called resources. Any CPU intensive process will use the server's CPU where you
are deploying your resources.</p>
<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
provider.</p>
</x-slot:explanation>

View File

@@ -14,7 +14,7 @@
@endif
<div id="screen" :class="fullscreen ? 'fullscreen' : ''">
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
class="relative flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto scrollbar border-coolgray-400"
class="relative flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-dotted rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
@@ -48,7 +48,7 @@
@foreach (decode_remote_command_output($application_deployment_queue) as $line)
<div @class([
'font-mono whitespace-pre-line',
'text-neutral-400' => $line['type'] == 'stdout',
'text-white' => $line['type'] == 'stdout',
'text-error' => $line['type'] == 'stderr',
'text-warning' => $line['hidden'],
])>[{{ $line['timestamp'] }}] @if ($line['hidden'])

View File

@@ -12,14 +12,14 @@
</form>
@forelse ($deployments as $deployment)
<a @class([
'bg-coolgray-200 p-2 border-l border-dashed transition-colors hover:no-underline',
'bg-coolgray-100 p-2 border-l border-dashed transition-colors hover:no-underline',
'hover:bg-coolgray-200' =>
data_get($deployment, 'status') === 'queued' ||
data_get($deployment, 'status') === 'cancelled by system',
data_get($deployment, 'status') === 'queued',
'border-warning hover:bg-warning hover:text-black' =>
data_get($deployment, 'status') === 'in_progress',
data_get($deployment, 'status') === 'in_progress' ||
data_get($deployment, 'status') === 'cancelled-by-user',
'border-error hover:bg-error' =>
data_get($deployment, 'status') === 'error',
data_get($deployment, 'status') === 'failed',
'border-success hover:bg-success' =>
data_get($deployment, 'status') === 'finished',
]) href="{{ $current_url . '/' . data_get($deployment, 'deployment_uuid') }}"

View File

@@ -27,10 +27,11 @@
<div class="flex gap-2">
<x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option>
<option value="static">Static</option>
<option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option>
</x-forms.select>
@if ($application->settings->is_static)
@if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required>
<option value="nginx:alpine">nginx:alpine</option>
<option disabled value="apache:alpine">apache:alpine</option>
@@ -51,7 +52,7 @@
@if ($application->could_set_build_commands())
@if ($application->build_pack === 'nixpacks')
<div>Nixpacks will detect the required configuration automatically.
<a class="underline" href="https://coolify.io/docs/frameworks">Framework Specific Docs</a>
<a class="underline" href="https://coolify.io/docs/frameworks/">Framework Specific Docs</a>
</div>
<div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="If you modify this, you probably need to have a nixpacks.toml"
@@ -97,7 +98,7 @@
@endif
<h3>Network</h3>
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->settings->is_static)
@if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
@else
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required

View File

@@ -48,7 +48,7 @@
@endif
</div>
@if ($application->previews->count() > 0)
<h4 class="py-4">Deployed Previews</h4>
<div class="pb-4">Previews</div>
<div class="flex gap-6 ">
@foreach ($application->previews as $preview)
<div class="flex flex-col p-4 bg-coolgray-200">
@@ -71,19 +71,19 @@
</a>
</div>
<div class="flex items-center gap-2 pt-6">
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
<x-forms.button class="bg-coolgray-500" wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited')
Deploy
@else
Redeploy
@endif
</x-forms.button>
<x-forms.button wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
<x-forms.button class="bg-coolgray-500" wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
Preview
</x-forms.button>
<a
href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
<x-forms.button>
<x-forms.button class="bg-coolgray-500">
Get Deployment Logs
</x-forms.button>
</a>

View File

@@ -182,7 +182,7 @@
</button>
@endif
@empty
<div>No service found.</div>
<div>No service found. Please try to reload the list!</div>
@endforelse
@endif
</div>

View File

@@ -16,19 +16,21 @@
<x-forms.input label="Description" id="application.description"></x-forms.input>
</div>
<div class="flex gap-2">
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
@if ($application->required_fqdn)
<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
<x-forms.input 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>
<x-forms.input 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>
@endif
@endif
<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>"
label="Image" id="application.image"></x-forms.input>
</div>
</div>
<h3 class="pt-2">Advanced</h3>
<div class="w-64">
<x-forms.checkbox instantSave label="Exclude from service status"

View File

@@ -12,10 +12,19 @@
<div class="flex gap-2">
<x-forms.input label="Name" id="database.human_name" placeholder="Name"></x-forms.input>
<x-forms.input label="Description" id="database.description"></x-forms.input>
<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>"
label="Image Tag" id="database.image"></x-forms.input>
</div>
<div class="flex gap-2">
<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>" label="Image Tag"
id="database.image"></x-forms.input>
<div class="flex items-end gap-2">
@if ($db_url_public)
<x-forms.input label="Database URL (public)"
helper="Your credentials are available in your environment variables." type="password" readonly
wire:model="db_url_public" />
@endif
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
</div>
<h3 class="pt-2">Advanced</h3>

View File

@@ -28,7 +28,7 @@
<div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'service-stack'">
<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)
<div @class([
'border-l border-dashed border-red-500' => Str::of(
@@ -58,7 +58,7 @@
@endif
<div class="text-xs">{{ $application->status }}</div>
</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
class="hover:text-warning">Logs</span></a>
</div>
@@ -88,7 +88,7 @@
@endif
<div class="text-xs">{{ $database->status }}</div>
</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
class="hover:text-warning">Logs</span></a>
</div>

View File

@@ -9,8 +9,20 @@
File</x-forms.button>
</div>
<div class="flex gap-2">
<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" />
</div>
@if ($fields)
<div>
<h3>Service Specific Configuration</h3>
</div>
<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>

View File

@@ -2,7 +2,9 @@
@if (
$resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql' ||
$resource->getMorphClass() == 'App\Models\StandaloneRedis')
$resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
$resource->getMorphClass() == 'App\Models\StandaloneMariadb' ||
$resource->getMorphClass() == 'App\Models\StandaloneMongodb')
<div class="flex items-center gap-2">
<h2>Storages</h2>
<x-helper

View File

@@ -14,7 +14,7 @@
<x-forms.button type="submit">Refresh</x-forms.button>
</form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white scrollbar border-coolgray-300"
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg
class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -5,10 +5,12 @@
<div class="flex flex-col gap-4 min-w-fit">
<a :class="activeTab === 'general' && 'text-white'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($application->build_pack !== 'static')
<a :class="activeTab === 'environment-variables' && 'text-white'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment
Variables</a>
@endif
@if ($application->git_based())
<a :class="activeTab === 'source' && 'text-white'"
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@@ -16,21 +18,25 @@
<a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a>
@if ($application->build_pack !== 'static')
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a>
@endif
<a :class="activeTab === 'webhooks' && 'text-white'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a>
@if ($application->git_based())
@if ($application->git_based() && $application->build_pack !== 'static')
<a :class="activeTab === 'previews' && 'text-white'"
@click.prevent="activeTab = 'previews'; window.location.hash = 'previews'" href="#">Preview
Deployments
</a>
@endif
@if ($application->build_pack !== 'static')
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
@endif
<a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a>

View File

@@ -237,7 +237,7 @@ Route::post('/payments/stripe/events', function () {
try {
$webhookSecret = config('subscription.stripe_webhook_secret');
$signature = request()->header('Stripe-Signature');
$excludedPlans = config('subscription.stripe_excluded_plans');
$event = \Stripe\Webhook::constructEvent(
request()->getContent(),
$signature,
@@ -253,6 +253,10 @@ Route::post('/payments/stripe/events', function () {
switch ($type) {
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
send_internal_notification('Checkout session completed without client reference id.');
break;
}
$userId = Str::before($clientReferenceId, ':');
$teamId = Str::after($clientReferenceId, ':');
$subscriptionId = data_get($data, 'subscription');
@@ -282,12 +286,17 @@ Route::post('/payments/stripe/events', function () {
break;
case 'invoice.paid':
$customerId = data_get($data, 'customer');
$planId = data_get($data, 'lines.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (!$subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
}
$planId = data_get($data, 'lines.data.0.plan.id');
$subscription->update([
'stripe_plan_id' => $planId,
'stripe_invoice_paid' => true,
@@ -303,11 +312,15 @@ Route::post('/payments/stripe/events', function () {
break;
case 'customer.subscription.updated':
$customerId = data_get($data, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$status = data_get($data, 'status');
$subscriptionId = data_get($data, 'items.data.0.subscription');
$planId = data_get($data, 'items.data.0.plan.id');
if (Str::contains($excludedPlans, $planId)) {
send_internal_notification('Subscription excluded.');
break;
}
$subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail();
$trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended');
$cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
$alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end');
$feedback = data_get($data, 'cancellation_details.feedback');

View File

@@ -39,12 +39,12 @@ module.exports = {
themes: [
{
coollabs: {
primary: "#323232",
primary: "#202020",
"primary-focus": "#242424",
secondary: "#6B16ED",
accent: "#4338ca",
neutral: "#1B1D1D",
"base-100": "#181818",
"base-100": "#101010",
info: "#2563EB",
success: "#16A34A",
warning: "#FCD34D",

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.
# tags: feedback, user-feedback

View File

@@ -7,14 +7,9 @@ services:
image: quay.io/minio/minio:latest
command: server /data --console-address ":9001"
environment:
SERVICE_FQDN_MINIO_9000:
SERVICE_FQDN_CONSOLE_9001:
MINIO_ROOT_USER: $SERVICE_USER_MINIO
MINIO_ROOT_PASSWORD: $SERVICE_PASSWORD_MINIO
- MINIO_SERVER_URL=$MINIO_SERVER_URL
- MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -132,7 +132,7 @@
]
},
"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.",
"compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=",
"tags": [
@@ -303,7 +303,7 @@
"minio": {
"documentation": "https:\/\/docs.min.io\/docs\/minio-docker-quickstart-guide.html",
"slogan": "MinIO is a high performance object storage server compatible with Amazon S3 APIs.",
"compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFNFUlZJQ0VfRlFETl9NSU5JT185MDAwOiBudWxsCiAgICAgIFNFUlZJQ0VfRlFETl9DT05TT0xFXzkwMDE6IG51bGwKICAgICAgTUlOSU9fUk9PVF9VU0VSOiAkU0VSVklDRV9VU0VSX01JTklPCiAgICAgIE1JTklPX1JPT1RfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5pby1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwMDAvbWluaW8vaGVhbHRoL2xpdmUnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
"compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScK",
"tags": [
"object",
"storage",

View File

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