Compare commits

...

25 Commits

Author SHA1 Message Date
Andras Bacsai
f16d0f650f Merge pull request #1596 from coollabsio/next
v4.0.0-beta.178
2024-01-02 17:23:11 +01:00
Andras Bacsai
cb80341a78 Fix customLabels assignment when proxyType is TRAEFIK_V2 2024-01-02 17:22:44 +01:00
Andras Bacsai
83d96c8d11 Refactor custom labels handling in General.php and update Docker Compose Content label in general.blade.php 2024-01-02 17:14:52 +01:00
Andras Bacsai
a8ca57d095 Update link in helper message in general.blade.php 2024-01-02 16:47:05 +01:00
Andras Bacsai
2d936a4b22 add compose link 2024-01-02 16:46:08 +01:00
Andras Bacsai
0653eb8511 set custom labels on every app 2024-01-02 16:44:41 +01:00
Andras Bacsai
cc64132627 Update README.md 2024-01-02 14:06:08 +01:00
Andras Bacsai
e0391e5abd Merge pull request #1594 from coollabsio/next
v4.0.0-beta.177
2024-01-02 13:58:30 +01:00
Andras Bacsai
025135bd2a feat: raw docker compose deployments 2024-01-02 13:55:35 +01:00
Andras Bacsai
5e5873a08d fix: duplicate compose variable 2024-01-02 11:46:02 +01:00
Andras Bacsai
14652c2878 Update sponsor logos and links 2024-01-02 07:31:54 +01:00
Andras Bacsai
a7ef5c456d Merge pull request #1592 from coollabsio/next
v4.0.0-beta.176
2023-12-31 10:52:15 +01:00
Andras Bacsai
117f1d4155 fix: horizon 2023-12-30 15:30:57 +01:00
Andras Bacsai
075f3ce930 remove unnecessary queue 2023-12-30 15:29:26 +01:00
Andras Bacsai
d5b3e88fc4 Merge pull request #1576 from coollabsio/next
v4.0.0-beta.175
2023-12-30 15:00:05 +01:00
Andras Bacsai
ba55e0c1bb feat: add environment description + able to change name 2023-12-30 14:47:26 +01:00
Andras Bacsai
54671354f0 fix: deploy key + docker compose 2023-12-30 14:20:02 +01:00
Andras Bacsai
a2c7e8d455 Merge pull request #1566 from stooit/fix/deploy-key-docker-compose
fix: Resolves deployment of docker compose applications when using a deploy key
2023-12-30 13:27:25 +01:00
Andras Bacsai
32dbdf5204 Fix redirect route in DecideWhatToDoWithUser middleware 2023-12-28 22:26:21 +01:00
Andras Bacsai
019887739c fix: wrong env variable parsing 2023-12-28 17:53:47 +01:00
Andras Bacsai
5596e41f2b fix: sub 2023-12-28 13:43:03 +01:00
Andras Bacsai
6a73a00a1f Merge pull request #1575 from coollabsio/next
v4.0.0-beta.174
2023-12-27 23:09:14 +01:00
Andras Bacsai
4121c9dd8b fix 2023-12-27 23:07:39 +01:00
Andras Bacsai
e4781dc129 fix: restore falsely deleted coolify-db-backup 2023-12-27 23:06:22 +01:00
Stuart Rowlands
d9599da4a8 Fix git clone command for deploy key + docker compose. 2023-12-21 11:16:03 -08:00
24 changed files with 338 additions and 119 deletions

View File

@@ -19,10 +19,12 @@ Thank you so much!
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="appwrite logo" width="200"/></a>
<a href="https://cccareers.org/" target="_blank"><img src="./other/logos/ccc-logo.webp" alt="cccareers logo" width="200"/></a>
<a href="https://appwrite.io" target="_blank"><img src="./other/logos/appwrite.svg" alt="appwrite logo" width="200"/></a>
## Github Sponsors ($15+)
<a href="https://bc.direct"><img width="60px" alt="BC Direct" src="https://github.com/coollabsio/coolify/assets/5845193/a4063c41-95ed-4a32-8814-cd1475572e37"/></a>
<a href="https://github.com/automazeio"><img src="https://github.com/automazeio.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/corentinclichy"><img src="https://github.com/corentinclichy.png" width="60px" alt="Corentin Clichy" /></a>
<a href="https://github.com/Niki2k1"><img src="https://github.com/Niki2k1.png" width="60px" alt="Niklas Lausch" /></a>
<a href="https://github.com/pixelinfinito"><img src="https://github.com/pixelinfinito.png" width="60px" alt="Pixel Infinito" /></a>
@@ -32,7 +34,6 @@ Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and
<a href="https://github.com/Illyism"><img src="https://github.com/Illyism.png" width="60px" alt="Ilias Ism" /></a>
<a href="https://github.com/urtho"><img src="https://github.com/urtho.png" width="60px" alt="Paweł Pierścionek" /></a>
<a href="https://github.com/monocursive"><img src="https://github.com/monocursive.png" width="60px" alt="Michael Mazurczak" /></a>
<a href="https://github.com/cccareers"><img src="https://github.com/cccareers.png" width="60px" alt="Creating Coding Careers" /></a>
## Organizations
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>

View File

@@ -7,6 +7,7 @@ use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
@@ -31,6 +32,9 @@ class Init extends Command
if ($cleanup) {
echo "Running cleanup\n";
$this->cleanup_stucked_resources();
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
// $this->cleanup_ssh();
}
$this->cleanup_in_progress_application_deployments();
@@ -51,6 +55,30 @@ class Init extends Command
}
}
}
private function restore_coolify_db_backup() {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
echo "Restoring coolify db backup\n";
$database->restore();
$scheduledBackup = ScheduledDatabaseBackup::find(0);
if (!$scheduledBackup) {
ScheduledDatabaseBackup::create([
'id' => 0,
'enabled' => true,
'save_s3' => false,
'frequency' => '0 0 * * *',
'database_id' => $database->id,
'database_type' => 'App\Models\StandalonePostgresql',
'team_id' => 0,
]);
}
}
} catch(\Throwable $e) {
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_helper_containers()
{
$servers = Server::all();
@@ -134,7 +162,7 @@ class Init extends Command
echo "Error in application: {$e->getMessage()}\n";
}
try {
$postgresqls = StandalonePostgresql::all();
$postgresqls = StandalonePostgresql::all()->where('id', '!=', 0);
foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) {
echo 'Postgresql without environment: ' . $postgresql->name . ' soft deleting\n';

View File

@@ -25,14 +25,14 @@ class DecideWhatToDoWithUser
if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) {
return $next($request);
}
return redirect()-route('verify.email');
return redirect()->route('verify.email');
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect()->route('subscription');
return redirect()->route('subscription.index');
}
}
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {

View File

@@ -446,8 +446,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_image_names();
$this->cleanup_git();
$this->application->loadComposeFile(isInit: false);
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$yaml = $composeFile = $this->application->docker_compose_raw;
} else {
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
}
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true

View File

@@ -66,6 +66,7 @@ class General extends Component
'application.settings.is_static' => 'boolean|required',
'application.docker_compose_custom_start_command' => 'nullable',
'application.docker_compose_custom_build_command' => 'nullable',
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -98,6 +99,7 @@ class General extends Component
'application.settings.is_static' => 'Is static',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_raw_compose_deployment_enabled' => 'Is raw compose deployment enabled',
];
public function mount()
{
@@ -114,6 +116,11 @@ class General extends Component
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
$this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
}
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates();
}
@@ -199,7 +206,12 @@ class General extends Component
public function submit($showToaster = true)
{
try {
ray($this->initialDockerComposeLocation, $this->application->docker_compose_location);
if (!$this->customLabels && $this->application->destination->server->proxyType() === 'TRAEFIK_V2') {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
}
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$this->loadComposeFile();
}
@@ -207,6 +219,7 @@ class General extends Component
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Livewire\Project;
use App\Models\Application;
use App\Models\Project;
use Livewire\Component;
class EnvironmentEdit extends Component
{
public Project $project;
public Application $application;
public $environment;
public array $parameters;
protected $rules = [
'environment.name' => 'required|min:3|max:255',
'environment.description' => 'nullable|min:3|max:255',
];
public function mount() {
$this->parameters = get_route_parameters();
$this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first();
$this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first();
}
public function submit()
{
$this->validate();
try {
$this->environment->save();
return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.environment-edit');
}
}

View File

@@ -55,7 +55,11 @@ class GithubPrivateRepositoryDeployKey extends Component
}
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
if (isDev()) {
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->get();
} else {
$this->private_keys = PrivateKey::where('team_id', currentTeam()->id)->where('id', '!=', 0)->get();
}
}
public function instantSave()

View File

@@ -382,6 +382,9 @@ class Application extends BaseModel
public function deploymentType()
{
if (isDev() && data_get($this, 'private_key_id') === 0) {
return 'deploy_key';
}
if (data_get($this, 'private_key_id')) {
return 'deploy_key';
} else if (data_get($this, 'source')) {
@@ -859,9 +862,12 @@ class Application extends BaseModel
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
if (!$only_checkout) {
if ($only_checkout) {
$git_clone_command = $git_clone_command_base;
} else {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
}
ray($git_clone_command);
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),
@@ -979,6 +985,7 @@ class Application extends BaseModel
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",

View File

@@ -41,11 +41,11 @@ function queue_application_deployment(int $application_id, string $deployment_uu
dispatch(new ApplicationDeploymentNewJob(
deployment: $deployment,
application: Application::find($application_id)
))->onConnection('long-running')->onQueue('long-running');
));
} else {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running');
));
}
}
@@ -57,11 +57,11 @@ function queue_next_deployment(Application $application, bool $isNew = false)
dispatch(new ApplicationDeploymentNewJob(
deployment: $next_found,
application: $application
))->onConnection('long-running')->onQueue('long-running');
));
} else {
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running');
));
}
}
}

View File

@@ -216,55 +216,60 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = collect([]);
$labels->push('traefik.enable=true');
foreach ($domains as $loop => $domain) {
$uuid = new Cuid2(7);
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
try {
$uuid = new Cuid2(7);
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
if (is_null($port) && !is_null($onlyPort)) {
$port = $onlyPort;
}
$http_label = "{$uuid}-{$loop}-http";
$https_label = "{$uuid}-{$loop}-https";
if ($schema === 'https') {
// Set labels for https
$labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$https_label}.entryPoints=https");
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$https_label}.service={$https_label}");
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
$labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix");
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
}
$labels->push("traefik.http.routers.{$https_label}.tls=true");
$labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
// Set labels for http (redirect to https)
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
if ($is_force_https_enabled) {
$labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
}
} else {
// Set labels for http
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
$labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix");
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
}
}
} catch(\Throwable $e) {
continue;
}
$http_label = "{$uuid}-{$loop}-http";
$https_label = "{$uuid}-{$loop}-https";
if ($schema === 'https') {
// Set labels for https
$labels->push("traefik.http.routers.{$https_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$https_label}.entryPoints=https");
$labels->push("traefik.http.routers.{$https_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$https_label}.service={$https_label}");
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
$labels->push("traefik.http.routers.{$https_label}.middlewares={$https_label}-stripprefix");
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
}
$labels->push("traefik.http.routers.{$https_label}.tls=true");
$labels->push("traefik.http.routers.{$https_label}.tls.certresolver=letsencrypt");
// Set labels for http (redirect to https)
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
if ($is_force_https_enabled) {
$labels->push("traefik.http.routers.{$http_label}.middlewares=redirect-to-https");
}
} else {
// Set labels for http
$labels->push("traefik.http.routers.{$http_label}.rule=Host(`{$host}`) && PathPrefix(`{$path}`)");
$labels->push("traefik.http.routers.{$http_label}.entryPoints=http");
$labels->push("traefik.http.routers.{$http_label}.middlewares=gzip");
if ($port) {
$labels->push("traefik.http.routers.{$http_label}.service={$http_label}");
$labels->push("traefik.http.services.{$http_label}.loadbalancer.server.port=$port");
}
if ($path !== '/') {
$labels->push("traefik.http.routers.{$http_label}.middlewares={$http_label}-stripprefix");
$labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}");
}
}
}
return $labels;

View File

@@ -102,6 +102,7 @@ function refreshSession(?Team $team = null): void
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
ray($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -227,11 +228,15 @@ function base_ip(): string
}
function getFqdnWithoutPort(String $fqdn)
{
$url = Url::fromString($fqdn);
$host = $url->getHost();
$scheme = $url->getScheme();
$path = $url->getPath();
return "$scheme://$host$path";
try {
$url = Url::fromString($fqdn);
$host = $url->getHost();
$scheme = $url->getScheme();
$path = $url->getPath();
return "$scheme://$host$path";
} catch (\Throwable $e) {
return $fqdn;
}
}
/**
* If fqdn is set, return it, otherwise return public ip.
@@ -1394,6 +1399,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$key = $value;
$defaultValue = null;
}
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'application_id' => $resource->id,
'is_preview' => false,
])->first();
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
@@ -1520,6 +1530,9 @@ function parseEnvVariable(Str|string $value)
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$port = $value->afterLast('_');
if (filter_var($port, FILTER_VALIDATE_INT) === false) {
$port = null;
}
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');

View File

@@ -184,19 +184,8 @@ return [
'connection' => 'redis',
'queue' => ['default'],
'balance' => 'auto',
'autoScalingStrategy' => 'time',
'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
'tries' => 1,
'timeout' => 300,
'nice' => 0,
],
'long-running' => [
'connection' => 'redis',
'queue' => ['long-running'],
'balance' => 'auto',
// 'autoScalingStrategy' => 'time',
// 'maxProcesses' => 1,
'maxTime' => 0,
'maxJobs' => 0,
'memory' => 128,
@@ -209,27 +198,15 @@ return [
'environments' => [
'production' => [
's6' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
],
],
'local' => [
's6' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 2),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
],
'long-running' => [
'autoScalingStrategy' => 'size',
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),

View File

@@ -33,14 +33,6 @@ return [
'sync' => [
'driver' => 'sync',
],
'long-running' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'long-running',
'retry_after' => 3600,
'block_for' => null,
'after_commit' => true,
],
'database' => [
'driver' => 'database',
'table' => 'jobs',

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

View File

@@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.173';
return '4.0.0-beta.178';

View File

@@ -0,0 +1,28 @@
<?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('environments', function (Blueprint $table) {
$table->string('description')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('environments', function (Blueprint $table) {
$table->dropColumn('description');
});
}
};

View File

@@ -0,0 +1,28 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_raw_compose_deployment_enabled')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_raw_compose_deployment_enabled');
});
}
};

View File

@@ -5,9 +5,10 @@
</div>
<div>Advanced configuration for your application.</div>
<div class="flex flex-col w-full pt-4">
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
@if (!$application->settings->is_raw_compose_deployment_enabled)
<x-forms.checkbox helper="Drain logs to your configured log drain endpoint in your Server settings."
instantSave id="application.settings.is_log_drain_enabled" label="Drain Logs" />
@endif
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="application.settings.is_force_https_enabled" label="Force Https" />

View File

@@ -48,7 +48,10 @@
</div>
@endif
@if ($application->build_pack === 'dockercompose')
@if (count($parsedServices) > 0)
<x-forms.checkbox instantSave id="application.settings.is_raw_compose_deployment_enabled"
label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a href='https://coolify.io/docs/docker-compose'>documentation.</a>" />
@if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image')))
<div class="flex items-end gap-2">
@@ -184,8 +187,13 @@
@endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
<x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." />
@if ($application->settings->is_raw_compose_deployment_enabled)
<x-forms.textarea rows="10" readonly id="application.docker_compose_raw"
label="Docker Compose Content (applicationId: {{$application->id}})" helper="You need to modify the docker compose file." />
@else
<x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." />
@endif
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif

View File

@@ -0,0 +1,44 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<div class="flex items-end gap-2">
<h1>Environment: {{ data_get($environment, 'name') }}</h1>
<x-forms.button type="submit">Save</x-forms.button>
</div>
<nav class="flex pt-2 pb-10">
<ol class="flex items-center">
<li class="inline-flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => request()->route('project_uuid')]) }}">
{{ $project->name }}</a>
</li>
<li>
<div class="flex items-center">
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resources', ['environment_name' => request()->route('environment_name'), 'project_uuid' => request()->route('project_uuid')]) }}">{{ request()->route('environment_name') }}</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
Edit
</div>
</li>
</ol>
</nav>
<div class="flex gap-2">
<x-forms.input label="Name" id="environment.name" />
<x-forms.input label="Description" id="environment.description" />
</div>
</form>
</div>

View File

@@ -3,13 +3,13 @@
<div class="flex items-center gap-2">
<h1>Resources</h1>
@if ($environment->isEmpty())
<a class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation"
<a class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation"
href="{{ route('project.clone', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => request()->route('environment_name')]) }}">
Clone
</a>
<livewire:project.delete-environment :environment_id="$environment->id" />
@else
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation">+
New</a>
<a class="font-normal text-white normal-case border-none rounded hover:no-underline btn btn-primary btn-sm no-animation"
@@ -41,12 +41,12 @@
</nav>
</div>
@if ($environment->isEmpty())
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
<a href="{{ route('project.resources.new', ['project_uuid' => request()->route('project_uuid'), 'environment_name' => request()->route('environment_name')]) }} "
class="items-center justify-center box">+ Add New Resource</a>
@endif
<div class="grid gap-2 lg:grid-cols-2">
@foreach ($environment->applications->sortBy('name') as $application)
<a class="relative box group"
<a class="relative box group"
href="{{ route('project.application.configuration', [$project->uuid, $environment->name, $application->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $application->name }}</div>
@@ -62,7 +62,7 @@
</a>
@endforeach
@foreach ($environment->databases()->sortBy('name') as $database)
<a class="relative box group"
<a class="relative box group"
href="{{ route('project.database.configuration', [$project->uuid, $environment->name, $database->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $database->name }}</div>
@@ -78,7 +78,7 @@
</a>
@endforeach
@foreach ($environment->services->sortBy('name') as $service)
<a class="relative box group"
<a class="relative box group"
href="{{ route('project.service.configuration', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div>

View File

@@ -10,12 +10,35 @@
<div class="text-xs truncate subtitle lg:text-sm">{{ $project->name }}</div>
<div class="grid gap-2 lg:grid-cols-2">
@forelse ($project->environments as $environment)
<a class="items-center justify-center font-bold box"
href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
{{ $environment->name }}
</a>
<div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')">
<a class="flex flex-col flex-1 mx-6 hover:no-underline"
href="{{ route('project.resources', [$project->uuid, $environment->name]) }}">
<div class="font-bold text-white"> {{ $environment->name }}</div>
<div class="description ">
{{ $environment->description }}</div>
</a>
<div class="flex items-center">
<a class="mx-4 rounded group-hover:text-white"
href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon hover:text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
</svg>
</a>
</div>
</div>
@empty
<p>No environments found.</p>
@endforelse
<script>
function goto(projectUuid, environmentName) {
window.location.href = '/project/' + projectUuid + '/' + environmentName;
}
</script>
</div>
</x-layout>

View File

@@ -13,6 +13,7 @@ use App\Livewire\Project\Service\Show as ServiceShow;
use App\Livewire\Dev\Compose as Compose;
use App\Livewire\Dashboard;
use App\Livewire\Project\CloneProject;
use App\Livewire\Project\EnvironmentEdit;
use App\Livewire\Project\Shared\ExecuteContainerCommand;
use App\Livewire\Project\Shared\Logs;
use App\Livewire\Security\ApiTokens;
@@ -113,6 +114,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/project/{project_uuid}/{environment_name}/new', [ProjectController::class, 'new'])->name('project.resources.new');
Route::get('/project/{project_uuid}/{environment_name}', [ProjectController::class, 'resources'])->name('project.resources');
Route::get('/project/{project_uuid}/{environment_name}/edit', EnvironmentEdit::class)->name('project.environment.edit');
// Applications
Route::get('/project/{project_uuid}/{environment_name}/application/{application_uuid}', ApplicationConfiguration::class)->name('project.application.configuration');

View File

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