Files
coolify/app/Traits/HasSecretManager.php
T
Andras Bacsai d1cf3e6ef1 fix(secrets): keep the secret manager source when cloning resources
Cloning an application, database, service or environment did not copy
the secret manager link, so variables with remote secret references
failed on the next deployment with "no secret manager source is
configured".

Clones now get a copy of the link when the integration token belongs to
the clone's team. Server transfer does not export integration tokens or
links; the export now warns which resources need the token and link
again on the target instance.
2026-10-01 07:16:45 +02:00

131 lines
4.4 KiB
PHP

<?php
namespace App\Traits;
use App\Models\EnvironmentVariable;
use App\Models\SecretManagerLink;
use App\Support\RemoteSecretReferences;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use RuntimeException;
trait HasSecretManager
{
/** @var array<string, string>|null */
private ?array $resolvedSecretManagerValues = null;
public static function bootHasSecretManager(): void
{
// Return nothing: a non-null result halts the "deleting" event and skips later listeners.
static::deleting(function ($resource): void {
$resource->secretManagerLink()->delete();
});
}
public function secretManagerLink(): MorphOne
{
return $this->morphOne(SecretManagerLink::class, 'resourceable');
}
/**
* Give a cloned resource the same secret manager source. The token is team-scoped, so a clone in another team gets no link.
*/
public function cloneSecretManagerLinkTo(Model $target): void
{
$link = $this->secretManagerLink()->with('integrationToken')->first();
// fresh(): a replicated model keeps the relations of its source, such as the old environment.
$targetTeamId = $target->fresh()?->team()?->id;
if (! $link || $targetTeamId === null || (int) $link->integrationToken?->team_id !== (int) $targetTeamId) {
return;
}
$target->secretManagerLink()->create([
'integration_token_id' => $link->integration_token_id,
'settings' => $link->settings,
]);
}
public function resolveSecretManagerEnvironmentVariable(EnvironmentVariable $environmentVariable): ?string
{
$value = $this->resolveSecretManagerEnvironmentVariableValue($environmentVariable);
return $this->formatEnvironmentVariableValue($environmentVariable, $value);
}
public function formatEnvironmentVariableValue(EnvironmentVariable $environmentVariable, ?string $value): ?string
{
if ($value === null) {
return null;
}
if (json_validate($value) && (str_starts_with($value, '{') || str_starts_with($value, '['))) {
return $value;
}
return $environmentVariable->is_literal || $environmentVariable->is_multiline
? "'{$value}'"
: escapeEnvVariables($value);
}
public function resolveSecretManagerEnvironmentVariableValue(EnvironmentVariable $environmentVariable): ?string
{
$value = $this->resolvedEnvironmentVariableValue($environmentVariable);
if ($value === null) {
return null;
}
if (RemoteSecretReferences::containsReference($value)) {
$secrets = $this->secretManagerValues();
$missing = RemoteSecretReferences::missingKeys($value, $secrets);
if ($missing !== []) {
throw new RuntimeException('Missing secret keys: '.implode(', ', $missing)." (referenced by {$environmentVariable->key}).");
}
$value = RemoteSecretReferences::substitute($value, $secrets);
}
return $value;
}
public function environmentVariableUsesSecretManager(EnvironmentVariable $environmentVariable): bool
{
return RemoteSecretReferences::containsReference(
$this->resolvedEnvironmentVariableValue($environmentVariable),
);
}
private function resolvedEnvironmentVariableValue(EnvironmentVariable $environmentVariable): ?string
{
return $environmentVariable->get_real_environment_variables_with_server(
$environmentVariable->value,
$this,
data_get($this, 'server'),
);
}
/** @return array<string, string> */
private function secretManagerValues(): array
{
if ($this->resolvedSecretManagerValues !== null) {
return $this->resolvedSecretManagerValues;
}
$link = $this->secretManagerLink()->with('integrationToken')->first();
if (! $link) {
throw new RuntimeException('Environment variables reference remote secrets, but no secret manager source is configured.');
}
return $this->resolvedSecretManagerValues = $link->fetchSecrets();
}
/** @return array<string, string> */
public function resolvedSecretManagerValuesForRedaction(): array
{
return $this->resolvedSecretManagerValues ?? [];
}
}