mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
fix(env): keep the trailing newline of multiline environment variables
Closes #9438 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
71814b4541
commit
52a78cdb0f
5 files changed
+104
-8
No files matched your search
@@ -16,10 +16,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
### Changed
|
||||
|
||||
- Stop the notification storm from scheduled jobs that keep failing. A failed scheduled task, database backup, storage backup, or Docker cleanup now notifies on the first failure, then at most once a day while it keeps failing. The first successful run after a failure sends one "works again" notification to the failure channels. A database backup whose S3 upload keeps failing is also notified at most once a day
|
||||
- Disable a scheduled task automatically when it failed 10 times in a row and its first failure is more than 24 hours old, and send one notification about it. The task page shows why the task was disabled. Enable the task again to clear the reason. The scheduled tasks API returns `consecutive_failures`, `failing_since`, and `disabled_reason`. Backups and Docker cleanup are never disabled automatically, so they start again by themselves after an outage
|
||||
- Skip a scheduled task when its application or service has no running container, instead of a failed run with "No containers running". A skipped run sends no notification and does not count toward the automatic disable
|
||||
- Move **Import Backup** from the resource menu to an **Import backup** button on the **Backups** page of databases and services. The button opens the import in a dialog. Old **Import Backup** links open the **Backups** page ([#11818](https://github.com/coollabsio/coolify/discussions/11818))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Keep the trailing newline of multiline environment variables, for example an SSH private key that a Dockerfile mounts as a build secret. Before, Coolify removed the edge whitespace of every value, so OpenSSH rejected the key with "error in libcrypto". A value with more than one line is now saved exactly as you enter it, in the UI and in the API. Single-line values still lose their edge whitespace. Save a variable again to add the newline that Coolify removed before ([#9438](https://github.com/coollabsio/coolify/issues/9438))
|
||||
- Use the same `COOLIFY_BRANCH` value in all parts of a preview deployment: `pull/<pull request number>/head`. Before, a variable that refers to `${COOLIFY_BRANCH}` got the branch of the application (for example `main`), but the container got `pull/<number>/head`. Other build packs got `pull/<number>/head:pr-<number>-coolify`, and applications from a public repository got the branch of the application ([#9572](https://github.com/coollabsio/coolify/issues/9572))
|
||||
- Send the **Resource status changes** notification when an application, preview, service, or database stops unexpectedly, for example after a crash or `docker kill`. Before, Coolify sent this notification only when it restarted the proxy or a database TCP proxy. Coolify waits 2 minutes and sends the notification only when the resource is still stopped, so a container that Docker restarts, or a deployment that replaces it, sends none. A stop, restart, or redeploy from Coolify sends no notification, and the same resource sends at most one notification in 15 minutes ([#9493](https://github.com/coollabsio/coolify/issues/9493))
|
||||
- Fix the "Network error" when you sign in with email in AppFlowy Web. AppFlowy Web runs on its own domain and sends a `Redirect_to` header to GoTrue on the admin domain, and GoTrue blocked the CORS preflight because it did not allow that header. The AppFlowy service template now sets `GOTRUE_CORS_ALLOWED_HEADERS=redirect_to`. For an existing AppFlowy service, add this variable to the `gotrue` service in the Docker Compose file and restart the service ([#10545](https://github.com/coollabsio/coolify/issues/10545))
|
||||
|
||||
@@ -15,5 +15,8 @@ class TrimStrings extends Middleware
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
// Environment variable values: the model trims single-line values and keeps multiline content exact.
|
||||
'value',
|
||||
'data.*.value',
|
||||
];
|
||||
}
|
||||
@@ -747,7 +747,7 @@ class EnvironmentVariable extends BaseModel
|
||||
if (is_null($environment_variable) || $environment_variable === '' || is_null($resource)) {
|
||||
return $environment_variable;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
$environment_variable = self::withoutEdgeWhitespace($environment_variable);
|
||||
$sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/');
|
||||
if ($sharedEnvsFound->isEmpty()) {
|
||||
return $environment_variable;
|
||||
@@ -800,7 +800,19 @@ class EnvironmentVariable extends BaseModel
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim(decrypt($environment_variable));
|
||||
return self::withoutEdgeWhitespace(decrypt($environment_variable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-line values lose their edge whitespace. Multiline content (a private key, a
|
||||
* certificate, a config file) is kept as it is, because tools such as OpenSSH reject a key
|
||||
* without its trailing newline.
|
||||
*/
|
||||
private static function withoutEdgeWhitespace(string $value): string
|
||||
{
|
||||
$trimmed = trim($value);
|
||||
|
||||
return str_contains($trimmed, "\n") ? $value : $trimmed;
|
||||
}
|
||||
|
||||
private function set_environment_variables(?string $environment_variable = null): ?string
|
||||
@@ -808,13 +820,8 @@ class EnvironmentVariable extends BaseModel
|
||||
if (is_null($environment_variable)) {
|
||||
return null;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
$type = str($environment_variable)->after('{{')->before('.')->value;
|
||||
if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) {
|
||||
return encrypt($environment_variable);
|
||||
}
|
||||
|
||||
return encrypt($environment_variable);
|
||||
return encrypt(self::withoutEdgeWhitespace($environment_variable));
|
||||
}
|
||||
|
||||
protected function key(): Attribute
|
||||
|
||||
@@ -644,3 +644,33 @@ describe('env PATCH keeps the value when value is omitted', function () {
|
||||
'database bulk' => ['database', 'databases', true],
|
||||
]);
|
||||
});
|
||||
|
||||
describe('env API keeps multiline values exact', function () {
|
||||
test('create and bulk update keep the trailing newline of a private key', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
$key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----\n";
|
||||
$headers = ['Authorization' => 'Bearer '.$this->bearerToken];
|
||||
|
||||
$this->withHeaders($headers)->postJson("/api/v1/applications/{$application->uuid}/envs", [
|
||||
'key' => 'SSH_KEY',
|
||||
'value' => $key,
|
||||
'is_multiline' => true,
|
||||
])->assertStatus(201);
|
||||
|
||||
$this->withHeaders($headers)->patchJson("/api/v1/applications/{$application->uuid}/envs/bulk", [
|
||||
'data' => [
|
||||
['key' => 'OTHER_KEY', 'value' => $key, 'is_multiline' => true],
|
||||
['key' => 'PLAIN', 'value' => " plain \n"],
|
||||
],
|
||||
])->assertStatus(201);
|
||||
|
||||
$values = $application->environment_variables()->get()->pluck('value', 'key');
|
||||
expect($values['SSH_KEY'])->toBe($key)
|
||||
->and($values['OTHER_KEY'])->toBe($key)
|
||||
->and($values['PLAIN'])->toBe('plain');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$team = Team::factory()->create();
|
||||
$project = Project::factory()->create(['team_id' => $team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$this->application = Application::factory()->create(['environment_id' => $environment->id]);
|
||||
});
|
||||
|
||||
function createVariable(Application $application, string $value, bool $isMultiline = false): EnvironmentVariable
|
||||
{
|
||||
$env = EnvironmentVariable::create([
|
||||
'key' => 'TEST_VAR',
|
||||
'value' => $value,
|
||||
'is_multiline' => $isMultiline,
|
||||
'is_literal' => true,
|
||||
'resourceable_id' => $application->id,
|
||||
'resourceable_type' => $application->getMorphClass(),
|
||||
]);
|
||||
|
||||
return $env->refresh();
|
||||
}
|
||||
|
||||
it('keeps the trailing newline of a multiline value such as a private key', function () {
|
||||
$key = "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAA\n-----END OPENSSH PRIVATE KEY-----\n";
|
||||
|
||||
$env = createVariable($this->application, $key, isMultiline: true);
|
||||
|
||||
expect($env->value)->toBe($key)
|
||||
->and($env->get_real_environment_variables_with_server($env->value, $this->application))->toBe($key);
|
||||
});
|
||||
|
||||
it('keeps the edge whitespace of multiline content exactly', function () {
|
||||
$value = " indented: true\nnext: line\n\n";
|
||||
|
||||
expect(createVariable($this->application, $value)->value)->toBe($value);
|
||||
});
|
||||
|
||||
it('still trims single-line values, also when they end with a newline', function () {
|
||||
expect(createVariable($this->application, ' value ')->value)->toBe('value')
|
||||
->and(createVariable($this->application, "value\n")->value)->toBe('value')
|
||||
->and(createVariable($this->application, "\n value \r\n")->value)->toBe('value');
|
||||
});
|
||||
Reference in new issue
Block a user