mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 10:14:47 +00:00
Merge remote-tracking branch 'origin/next' into feature/vultr-cloud-provider
This commit is contained in:
commit
c58b4fd69c
718 files changed
+30292
-7279
No files matched your search
+116
-112
@@ -23,7 +23,6 @@ use RuntimeException;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Application model',
|
||||
@@ -177,11 +176,8 @@ class Application extends BaseModel
|
||||
'manual_webhook_secret_bitbucket',
|
||||
'manual_webhook_secret_gitea',
|
||||
'docker_compose_location',
|
||||
'docker_compose_pr_location',
|
||||
'docker_compose',
|
||||
'docker_compose_pr',
|
||||
'docker_compose_raw',
|
||||
'docker_compose_pr_raw',
|
||||
'docker_compose_domains',
|
||||
'docker_compose_custom_start_command',
|
||||
'docker_compose_custom_build_command',
|
||||
@@ -219,6 +215,24 @@ class Application extends BaseModel
|
||||
|
||||
protected $appends = ['server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability. Internal serializers (deployment
|
||||
* job, compose generation) must makeVisible explicitly before toArray().
|
||||
*/
|
||||
protected $hidden = [
|
||||
'http_basic_auth_password',
|
||||
'manual_webhook_secret_github',
|
||||
'manual_webhook_secret_gitlab',
|
||||
'manual_webhook_secret_bitbucket',
|
||||
'manual_webhook_secret_gitea',
|
||||
'dockerfile',
|
||||
'docker_compose',
|
||||
'docker_compose_raw',
|
||||
'custom_labels',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
@@ -1267,9 +1281,9 @@ class Application extends BaseModel
|
||||
{
|
||||
$newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->custom_network_aliases.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->settings?->use_build_secrets.$this->settings?->inject_build_args_to_dockerfile.$this->settings?->include_source_commit_in_build);
|
||||
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->makeVisible('value')->sort());
|
||||
} else {
|
||||
$newConfigHash .= json_encode($this->environment_variables_preview()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables_preview()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->makeVisible('value')->sort());
|
||||
}
|
||||
|
||||
return md5($newConfigHash);
|
||||
@@ -1339,7 +1353,7 @@ class Application extends BaseModel
|
||||
{
|
||||
try {
|
||||
['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false);
|
||||
instant_remote_process([$lsRemoteCommand], $this->destination->server, true);
|
||||
instant_remote_process([$this->gitCommandsAsShellCommand($lsRemoteCommand)], $this->destination->server, true);
|
||||
|
||||
return [
|
||||
'is_accessible' => true,
|
||||
@@ -1391,13 +1405,13 @@ class Application extends BaseModel
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
$commands->push($this->gitCommand($base_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1414,29 +1428,16 @@ class Application extends BaseModel
|
||||
$escapedCustomRepository = str_replace("'", "'\\''", $customRepository);
|
||||
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$gitlabPort} -o Port={$gitlabPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" {$base_command} '{$escapedCustomRepository}'";
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands = collect([
|
||||
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
|
||||
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
|
||||
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"trap 'rm -f {$customSshKeyLocation}' EXIT",
|
||||
'mkdir -p /root/.ssh',
|
||||
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
|
||||
"chmod 600 {$customSshKeyLocation}",
|
||||
]);
|
||||
}
|
||||
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
$commands->push($this->gitCommand($base_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $commands,
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1448,13 +1449,13 @@ class Application extends BaseModel
|
||||
$base_command = "{$base_command} {$escapedCustomRepository}";
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
$commands->push($this->gitCommand($base_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1473,29 +1474,16 @@ class Application extends BaseModel
|
||||
$escapedCustomRepository = str_replace("'", "'\\''", $customRepository);
|
||||
$base_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$customSshKeyLocation} -o IdentitiesOnly=yes\" {$base_command} '{$escapedCustomRepository}'";
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands = collect([
|
||||
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
|
||||
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
|
||||
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"trap 'rm -f {$customSshKeyLocation}' EXIT",
|
||||
'mkdir -p /root/.ssh',
|
||||
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
|
||||
"chmod 600 {$customSshKeyLocation}",
|
||||
]);
|
||||
}
|
||||
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
$commands->push($this->gitCommand($base_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $commands,
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1507,19 +1495,60 @@ class Application extends BaseModel
|
||||
$base_command = "{$base_command} {$escapedCustomRepository}";
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $base_command)));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
$commands->push($this->gitCommand($base_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function gitCommand(string $command): array
|
||||
{
|
||||
return [
|
||||
'command' => $command,
|
||||
'hidden' => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function gitCommandsAsShellCommand(Collection|array|string $commands): string
|
||||
{
|
||||
if (is_string($commands)) {
|
||||
return $commands;
|
||||
}
|
||||
|
||||
return collect($commands)
|
||||
->map(fn ($command) => data_get($command, 'command') ?? $command[0] ?? $command)
|
||||
->implode(' && ');
|
||||
}
|
||||
|
||||
private function gitSshKeySetupCommands(string $deploymentUuid, string $privateKey, bool $execInDocker): Collection
|
||||
{
|
||||
$customSshKeyLocation = "/root/.ssh/id_rsa_coolify_{$deploymentUuid}";
|
||||
$commands = collect([]);
|
||||
|
||||
if (! $execInDocker) {
|
||||
$commands->push($this->gitCommand("trap 'rm -f {$customSshKeyLocation}' EXIT"));
|
||||
}
|
||||
|
||||
$commands->push($this->gitCommand($execInDocker ? executeInDocker($deploymentUuid, 'mkdir -p /root/.ssh') : 'mkdir -p /root/.ssh'));
|
||||
$commands->push([
|
||||
'command' => $execInDocker
|
||||
? executeInDocker($deploymentUuid, "echo '{$privateKey}' | base64 -d | tee {$customSshKeyLocation} > /dev/null")
|
||||
: "echo '{$privateKey}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
|
||||
'hidden' => true,
|
||||
'skip_command_log' => true,
|
||||
]);
|
||||
$commands->push($this->gitCommand($execInDocker ? executeInDocker($deploymentUuid, "chmod 600 {$customSshKeyLocation}") : "chmod 600 {$customSshKeyLocation}"));
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
private function withGitHttpTransportConfig(?string $gitConfigOptions = null): string
|
||||
{
|
||||
return trim(($gitConfigOptions ? "{$gitConfigOptions} " : '').'-c http.version=HTTP/1.1');
|
||||
@@ -1591,9 +1620,9 @@ class Application extends BaseModel
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit, gitConfigOptions: $gitConfigOptions);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
} else {
|
||||
$github_access_token = generateGithubInstallationToken($this->source);
|
||||
@@ -1620,9 +1649,9 @@ class Application extends BaseModel
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false, commit: $commit, gitConfigOptions: $gitConfigOptions);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
}
|
||||
if ($pull_request_id !== 0) {
|
||||
@@ -1632,14 +1661,14 @@ class Application extends BaseModel
|
||||
$gitCommand = isset($gitConfigOptions) ? "git {$gitConfigOptions}" : 'git';
|
||||
$escapedPrBranch = escapeshellarg($branch);
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command")));
|
||||
} else {
|
||||
$commands->push("cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command");
|
||||
$commands->push($this->gitCommand("cd {$escapedBaseDir} && {$gitCommand} fetch origin {$escapedPrBranch} && $git_checkout_command"));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1662,39 +1691,26 @@ class Application extends BaseModel
|
||||
} else {
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $gitlabSshCommand);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands = collect([
|
||||
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
|
||||
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
|
||||
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"trap 'rm -f {$customSshKeyLocation}' EXIT",
|
||||
'mkdir -p /root/.ssh',
|
||||
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
|
||||
"chmod 600 {$customSshKeyLocation}",
|
||||
]);
|
||||
}
|
||||
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
|
||||
|
||||
if ($pull_request_id !== 0) {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$gitlabGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $gitlabSshCommand);
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $commands,
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1711,13 +1727,13 @@ class Application extends BaseModel
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true, commit: $commit, gitConfigOptions: $gitConfigOptions);
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1739,55 +1755,42 @@ class Application extends BaseModel
|
||||
} else {
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitSshCommand: $deployKeySshCommand);
|
||||
}
|
||||
if ($exec_in_docker) {
|
||||
$commands = collect([
|
||||
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
|
||||
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null"),
|
||||
executeInDocker($deployment_uuid, "chmod 600 {$customSshKeyLocation}"),
|
||||
]);
|
||||
} else {
|
||||
$commands = collect([
|
||||
"trap 'rm -f {$customSshKeyLocation}' EXIT",
|
||||
'mkdir -p /root/.ssh',
|
||||
"echo '{$private_key}' | base64 -d | tee {$customSshKeyLocation} > /dev/null",
|
||||
"chmod 600 {$customSshKeyLocation}",
|
||||
]);
|
||||
}
|
||||
$commands = $this->gitSshKeySetupCommands($deployment_uuid, $private_key, $exec_in_docker);
|
||||
if ($pull_request_id !== 0) {
|
||||
if ($git_type === 'gitlab') {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
|
||||
} elseif ($git_type === 'github' || $git_type === 'gitea') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $deployKeySshCommand);
|
||||
} elseif ($git_type === 'bitbucket') {
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && {$deployKeyGitSshCommand} ".$this->buildGitCheckoutCommand($commit, $deployKeySshCommand);
|
||||
}
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $commands,
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1808,37 +1811,37 @@ class Application extends BaseModel
|
||||
if ($git_type === 'gitlab') {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" {$gitCommand} fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand, $gitConfigOptions);
|
||||
} elseif ($git_type === 'github' || $git_type === 'gitea') {
|
||||
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" {$gitCommand} fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name, $otherSshCommand, $gitConfigOptions);
|
||||
} elseif ($git_type === 'bitbucket') {
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, "echo 'Checking out $branch'")));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out $branch'");
|
||||
$commands->push($this->gitCommand("echo 'Checking out $branch'"));
|
||||
}
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && GIT_SSH_COMMAND=\"{$otherSshCommand}\" ".$this->buildGitCheckoutCommand($commit, $otherSshCommand, $gitConfigOptions);
|
||||
}
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
$commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_clone_command)));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
$commands->push($this->gitCommand($git_clone_command));
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'commands' => $this->gitCommandsAsShellCommand($commands),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
@@ -1925,8 +1928,9 @@ class Application extends BaseModel
|
||||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
$uuid = new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: 'checkout');
|
||||
$cloneCommand = $this->gitCommandsAsShellCommand($cloneCommand);
|
||||
$cloneCommand = str_replace(' clone ', ' clone --quiet ', $cloneCommand);
|
||||
$workdir = rtrim($this->base_directory, '/');
|
||||
$composeFile = $this->docker_compose_location;
|
||||
|
||||
@@ -84,6 +84,7 @@ class ApplicationDeploymentQueue extends Model
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'logs',
|
||||
'configuration_snapshot',
|
||||
'configuration_diff',
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Models;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ApplicationPreview extends BaseModel
|
||||
{
|
||||
@@ -111,7 +110,7 @@ class ApplicationPreview extends BaseModel
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
@@ -173,7 +172,7 @@ class ApplicationPreview extends BaseModel
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
abstract class BaseModel extends Model
|
||||
{
|
||||
@@ -15,7 +14,7 @@ abstract class BaseModel extends Model
|
||||
static::creating(function (Model $model) {
|
||||
// Generate a UUID if one isn't set
|
||||
if (! $model->uuid) {
|
||||
$model->uuid = (string) new Cuid2;
|
||||
$model->uuid = new_public_id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ class CloudInitScript extends Model
|
||||
'script',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'script',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -15,6 +15,10 @@ class CloudProviderToken extends BaseModel
|
||||
'name',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'token' => 'encrypted',
|
||||
];
|
||||
|
||||
@@ -34,6 +34,10 @@ class DiscordNotificationSettings extends Model
|
||||
'discord_ping_enabled',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'discord_webhook_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'discord_enabled' => 'boolean',
|
||||
'discord_webhook_url' => 'encrypted',
|
||||
|
||||
@@ -43,6 +43,16 @@ class EmailNotificationSettings extends Model
|
||||
'traefik_outdated_email_notifications',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'smtp_from_address',
|
||||
'smtp_from_name',
|
||||
'smtp_recipients',
|
||||
'smtp_host',
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'smtp_enabled' => 'boolean',
|
||||
'smtp_from_address' => 'encrypted',
|
||||
|
||||
@@ -80,6 +80,16 @@ class EnvironmentVariable extends BaseModel
|
||||
|
||||
protected $appends = ['real_value', 'is_shared', 'is_really_required', 'is_buildpack_control', 'is_coolify'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'value',
|
||||
'real_value',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (ModelsEnvironmentVariable $environment_variable) {
|
||||
@@ -356,7 +366,7 @@ class EnvironmentVariable extends BaseModel
|
||||
|
||||
private function set_environment_variables(?string $environment_variable = null): ?string
|
||||
{
|
||||
if (is_null($environment_variable) && $environment_variable === '') {
|
||||
if (is_null($environment_variable)) {
|
||||
return null;
|
||||
}
|
||||
$environment_variable = trim($environment_variable);
|
||||
|
||||
@@ -46,6 +46,19 @@ class InstanceSettings extends Model
|
||||
'dev_helper_version',
|
||||
'is_wire_navigate_enabled',
|
||||
'is_mcp_server_enabled',
|
||||
'webhook_allowed_internal_hosts',
|
||||
'webhook_allow_localhost',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'smtp_from_address',
|
||||
'smtp_from_name',
|
||||
'smtp_recipients',
|
||||
'smtp_host',
|
||||
'smtp_username',
|
||||
'smtp_password',
|
||||
'resend_api_key',
|
||||
'sentinel_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -69,10 +82,16 @@ class InstanceSettings extends Model
|
||||
'sentinel_token' => 'encrypted',
|
||||
'is_wire_navigate_enabled' => 'boolean',
|
||||
'is_mcp_server_enabled' => 'boolean',
|
||||
'webhook_allowed_internal_hosts' => 'array',
|
||||
'webhook_allow_localhost' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::created(function () {
|
||||
Once::flush();
|
||||
});
|
||||
|
||||
static::updated(function ($settings) {
|
||||
// Clear once() cache so subsequent calls get fresh data
|
||||
Once::flush();
|
||||
|
||||
@@ -21,9 +21,14 @@ class LocalFileVolume extends BaseModel
|
||||
// 'mount_path' => 'encrypted',
|
||||
'content' => 'encrypted',
|
||||
'is_directory' => 'boolean',
|
||||
'is_host_file' => 'boolean',
|
||||
'is_preview_suffix_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'content',
|
||||
];
|
||||
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
@@ -33,6 +38,7 @@ class LocalFileVolume extends BaseModel
|
||||
'resource_type',
|
||||
'resource_id',
|
||||
'is_directory',
|
||||
'is_host_file',
|
||||
'chown',
|
||||
'chmod',
|
||||
'is_based_on_git',
|
||||
@@ -44,6 +50,10 @@ class LocalFileVolume extends BaseModel
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (LocalFileVolume $fileVolume) {
|
||||
if ($fileVolume->is_host_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileVolume->load(['service']);
|
||||
dispatch(new ServerStorageSaveJob($fileVolume));
|
||||
});
|
||||
@@ -70,6 +80,10 @@ class LocalFileVolume extends BaseModel
|
||||
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
if ($this->is_host_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
@@ -124,6 +138,10 @@ class LocalFileVolume extends BaseModel
|
||||
|
||||
public function deleteStorageOnServer()
|
||||
{
|
||||
if ($this->is_host_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
@@ -161,6 +179,10 @@ class LocalFileVolume extends BaseModel
|
||||
|
||||
public function saveStorageOnServer()
|
||||
{
|
||||
if ($this->is_host_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->load(['service']);
|
||||
$isService = data_get($this->resource, 'service');
|
||||
if ($isService) {
|
||||
@@ -171,26 +193,26 @@ class LocalFileVolume extends BaseModel
|
||||
$server = $this->resource->destination->server;
|
||||
}
|
||||
$commands = collect([]);
|
||||
|
||||
// Validate fs_path early before any shell interpolation
|
||||
validateShellSafePath($this->fs_path, 'storage path');
|
||||
$escapedFsPath = escapeshellarg($this->fs_path);
|
||||
$escapedWorkdir = escapeshellarg($workdir);
|
||||
|
||||
if ($this->is_directory) {
|
||||
// Validate fs_path early before any shell interpolation
|
||||
validateShellSafePath($this->fs_path, 'storage path');
|
||||
$escapedFsPath = escapeshellarg($this->fs_path);
|
||||
$commands->push("mkdir -p {$escapedFsPath} > /dev/null 2>&1 || true");
|
||||
$commands->push("mkdir -p {$escapedWorkdir} > /dev/null 2>&1 || true");
|
||||
$commands->push("cd {$escapedWorkdir}");
|
||||
}
|
||||
if (str($this->fs_path)->startsWith('.') || str($this->fs_path)->startsWith('/') || str($this->fs_path)->startsWith('~')) {
|
||||
$parent_dir = str($this->fs_path)->beforeLast('/');
|
||||
$path = data_get_str($this, 'fs_path');
|
||||
$content = data_get($this, 'content');
|
||||
$pathForParentDirectory = str($this->fs_path);
|
||||
if ($pathForParentDirectory->startsWith('.') || $pathForParentDirectory->startsWith('/') || $pathForParentDirectory->startsWith('~')) {
|
||||
$parent_dir = $pathForParentDirectory->beforeLast('/');
|
||||
if ($parent_dir != '') {
|
||||
$escapedParentDir = escapeshellarg($parent_dir);
|
||||
$commands->push("mkdir -p {$escapedParentDir} > /dev/null 2>&1 || true");
|
||||
}
|
||||
}
|
||||
$path = data_get_str($this, 'fs_path');
|
||||
$content = data_get($this, 'content');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$path = $workdir.$path;
|
||||
@@ -348,7 +370,6 @@ class LocalFileVolume extends BaseModel
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage(), 'Error checking read-only volume');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -187,7 +187,6 @@ class LocalPersistentVolume extends BaseModel
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
ray($e->getMessage(), 'Error checking read-only persistent volume');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ class OauthSetting extends Model
|
||||
|
||||
protected $fillable = ['provider', 'client_id', 'client_secret', 'redirect_uri', 'tenant', 'base_url', 'enabled'];
|
||||
|
||||
protected $hidden = [
|
||||
'client_secret',
|
||||
];
|
||||
|
||||
protected function clientSecret(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -30,7 +31,7 @@ use phpseclib3\Crypt\PublicKeyLoader;
|
||||
)]
|
||||
class PrivateKey extends BaseModel
|
||||
{
|
||||
use HasSafeStringAttribute, WithRateLimiting;
|
||||
use HasFactory, HasSafeStringAttribute, WithRateLimiting;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
@@ -41,6 +42,10 @@ class PrivateKey extends BaseModel
|
||||
'fingerprint',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'private_key',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'private_key' => 'encrypted',
|
||||
];
|
||||
|
||||
+11
-4
@@ -5,8 +5,8 @@ namespace App\Models;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Project model',
|
||||
@@ -59,7 +59,7 @@ class Project extends BaseModel
|
||||
Environment::create([
|
||||
'name' => 'production',
|
||||
'project_id' => $project->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($project) {
|
||||
@@ -156,9 +156,16 @@ class Project extends BaseModel
|
||||
$this->services()->count() == 0;
|
||||
}
|
||||
|
||||
public function databases()
|
||||
public function databases(array $with = []): Collection
|
||||
{
|
||||
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
|
||||
return $this->postgresqls()->with($with)->get()
|
||||
->merge($this->redis()->with($with)->get())
|
||||
->merge($this->mongodbs()->with($with)->get())
|
||||
->merge($this->mysqls()->with($with)->get())
|
||||
->merge($this->mariadbs()->with($with)->get())
|
||||
->merge($this->keydbs()->with($with)->get())
|
||||
->merge($this->dragonflies()->with($with)->get())
|
||||
->merge($this->clickhouses()->with($with)->get());
|
||||
}
|
||||
|
||||
public function navigateTo()
|
||||
|
||||
@@ -34,6 +34,11 @@ class PushoverNotificationSettings extends Model
|
||||
'traefik_outdated_pushover_notifications',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'pushover_user_key',
|
||||
'pushover_api_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'pushover_enabled' => 'boolean',
|
||||
'pushover_user_key' => 'encrypted',
|
||||
|
||||
+37
-17
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Rules\SafeWebhookUrl;
|
||||
use App\Rules\ValidS3BucketName;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -31,6 +32,11 @@ class S3Storage extends BaseModel
|
||||
'unusable_email_sent',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'key',
|
||||
'secret',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_usable' => 'boolean',
|
||||
'key' => 'encrypted',
|
||||
@@ -147,12 +153,22 @@ class S3Storage extends BaseModel
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make(
|
||||
['endpoint' => $this['endpoint']],
|
||||
['endpoint' => ['required', new SafeWebhookUrl]],
|
||||
[
|
||||
'endpoint' => $this['endpoint'],
|
||||
'bucket' => $this['bucket'],
|
||||
],
|
||||
[
|
||||
'endpoint' => ['required', new SafeWebhookUrl],
|
||||
'bucket' => ['required', new ValidS3BucketName],
|
||||
],
|
||||
);
|
||||
if ($validator->fails()) {
|
||||
$validator->fails();
|
||||
if ($validator->errors()->has('endpoint')) {
|
||||
throw new \RuntimeException('S3 endpoint is not allowed: '.$validator->errors()->first('endpoint'));
|
||||
}
|
||||
if ($validator->errors()->has('bucket')) {
|
||||
throw new \RuntimeException('S3 bucket name is not allowed: '.$validator->errors()->first('bucket'));
|
||||
}
|
||||
|
||||
$disk = Storage::build([
|
||||
'driver' => 's3',
|
||||
@@ -162,10 +178,10 @@ class S3Storage extends BaseModel
|
||||
'bucket' => $this['bucket'],
|
||||
'endpoint' => $this['endpoint'],
|
||||
'use_path_style_endpoint' => true,
|
||||
'http' => [
|
||||
'http' => array_merge(SafeWebhookUrl::httpClientOptions($this['endpoint']), [
|
||||
'connect_timeout' => self::CONNECTION_TIMEOUT_SECONDS,
|
||||
'timeout' => self::REQUEST_TIMEOUT_SECONDS,
|
||||
],
|
||||
]),
|
||||
]);
|
||||
// Test the connection by listing files with ListObjectsV2 (S3)
|
||||
$disk->files();
|
||||
@@ -176,21 +192,25 @@ class S3Storage extends BaseModel
|
||||
$exception = $this->toUserFriendlyConnectionException($e);
|
||||
$this->is_usable = false;
|
||||
if ($this->unusable_email_sent === false && is_transactional_emails_enabled()) {
|
||||
$mail = new MailMessage;
|
||||
$mail->subject('Coolify: S3 Storage Connection Error');
|
||||
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $exception->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
|
||||
try {
|
||||
$mail = new MailMessage;
|
||||
$mail->subject('Coolify: S3 Storage Connection Error');
|
||||
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $exception->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
|
||||
|
||||
// Load the team with its members and their roles explicitly
|
||||
$team = $this->team()->with(['members' => function ($query) {
|
||||
$query->withPivot('role');
|
||||
}])->first();
|
||||
// Load the team with its members and their roles explicitly
|
||||
$team = $this->team()->with(['members' => function ($query) {
|
||||
$query->withPivot('role');
|
||||
}])->first();
|
||||
|
||||
// Get admins directly from the pivot relationship for this specific team
|
||||
$users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']);
|
||||
foreach ($users as $user) {
|
||||
send_user_an_email($mail, $user->email);
|
||||
// Get admins directly from the pivot relationship for this specific team
|
||||
$users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']);
|
||||
foreach ($users as $user) {
|
||||
send_user_an_email($mail, $user->email);
|
||||
}
|
||||
$this->unusable_email_sent = true;
|
||||
} catch (\Throwable $emailException) {
|
||||
\Log::warning('Failed to send S3 connection error notification: '.$emailException->getMessage());
|
||||
}
|
||||
$this->unusable_email_sent = true;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
|
||||
+11
-4
@@ -38,7 +38,6 @@ use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
|
||||
use Spatie\Url\Url;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* @property array{
|
||||
@@ -256,6 +255,16 @@ class Server extends BaseModel
|
||||
'force_disabled' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_newrelic_license_key',
|
||||
];
|
||||
|
||||
protected $schemalessAttributes = [
|
||||
'proxy',
|
||||
];
|
||||
@@ -1090,7 +1099,7 @@ $schema://$host {
|
||||
{
|
||||
$attributes = [
|
||||
'name' => 'coolify',
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
];
|
||||
@@ -1573,7 +1582,6 @@ $schema://$host {
|
||||
public function generateCaCertificate()
|
||||
{
|
||||
try {
|
||||
ray('Generating CA certificate for server', $this->id);
|
||||
SslHelper::generateSslCertificate(
|
||||
commonName: 'Coolify CA Certificate',
|
||||
serverId: $this->id,
|
||||
@@ -1581,7 +1589,6 @@ $schema://$host {
|
||||
validityDays: 10 * 365
|
||||
);
|
||||
$caCertificate = $this->sslCertificates()->where('is_ca_certificate', true)->first();
|
||||
ray('CA certificate generated', $caCertificate);
|
||||
if ($caCertificate) {
|
||||
$certificateContent = $caCertificate->ssl_certificate;
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
@@ -114,6 +114,20 @@ class ServerSetting extends Model
|
||||
'connection_timeout' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'sentinel_token',
|
||||
'sentinel_custom_url',
|
||||
'logdrain_newrelic_license_key',
|
||||
'logdrain_axiom_api_key',
|
||||
'logdrain_custom_config',
|
||||
'logdrain_custom_config_parser',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
|
||||
+24
-6
@@ -16,7 +16,6 @@ use OpenApi\Attributes as OA;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Service model',
|
||||
@@ -67,11 +66,22 @@ class Service extends BaseModel
|
||||
|
||||
protected $appends = ['server_status', 'status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability. Internal compose generators
|
||||
* must makeVisible explicitly before toArray().
|
||||
*/
|
||||
protected $hidden = [
|
||||
'docker_compose',
|
||||
'docker_compose_raw',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($service) {
|
||||
if (blank($service->name)) {
|
||||
$service->name = 'service-'.(new Cuid2);
|
||||
$service->name = 'service-'.new_public_id();
|
||||
}
|
||||
});
|
||||
static::created(function ($service) {
|
||||
@@ -95,7 +105,7 @@ class Service extends BaseModel
|
||||
$storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
|
||||
|
||||
$newConfigHash = $images.$domains.$images.$storages;
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
@@ -626,7 +636,7 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Unleash', $data->toArray());
|
||||
break;
|
||||
case $image->contains('grafana'):
|
||||
case $this->isGrafanaImage($image->toString()):
|
||||
$data = collect([]);
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_GRAFANA')->first();
|
||||
$data = $data->merge([
|
||||
@@ -1380,6 +1390,15 @@ class Service extends BaseModel
|
||||
return $fields;
|
||||
}
|
||||
|
||||
private function isGrafanaImage(string $image): bool
|
||||
{
|
||||
return in_array($image, [
|
||||
'grafana/grafana',
|
||||
'grafana/grafana-oss',
|
||||
'grafana/grafana-enterprise',
|
||||
], true);
|
||||
}
|
||||
|
||||
public function saveExtraFields($fields)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
@@ -1555,7 +1574,7 @@ class Service extends BaseModel
|
||||
"cd $workdir",
|
||||
], $this->server);
|
||||
|
||||
$filename = new Cuid2.'-docker-compose.yml';
|
||||
$filename = new_public_id().'-docker-compose.yml';
|
||||
Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose);
|
||||
$path = Storage::path("tmp/{$filename}");
|
||||
instant_scp($path, "{$workdir}/docker-compose.yml", $this->server);
|
||||
@@ -1575,7 +1594,6 @@ class Service extends BaseModel
|
||||
$envs->push('SERVICE_NAME_'.str($serviceName)->replace('-', '_')->replace('.', '_')->upper().'='.$serviceName);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,10 @@ class SharedEnvironmentVariable extends Model
|
||||
'version',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'value',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'key' => 'string',
|
||||
'value' => 'encrypted',
|
||||
|
||||
@@ -33,6 +33,10 @@ class SlackNotificationSettings extends Model
|
||||
'traefik_outdated_slack_notifications',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'slack_webhook_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'slack_enabled' => 'boolean',
|
||||
'slack_webhook_url' => 'encrypted',
|
||||
|
||||
@@ -20,6 +20,10 @@ class SslCertificate extends Model
|
||||
'is_ca_certificate',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'ssl_private_key',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ssl_certificate' => 'encrypted',
|
||||
'ssl_private_key' => 'encrypted',
|
||||
|
||||
@@ -54,6 +54,17 @@ class StandaloneClickhouse extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'clickhouse_admin_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -123,7 +134,7 @@ class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -144,6 +144,6 @@ class StandaloneDocker extends BaseModel
|
||||
|
||||
public function attachedTo()
|
||||
{
|
||||
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
|
||||
return $this->applications()->exists() || $this->databases()->count() > 0 || $this->services()->exists();
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,17 @@ class StandaloneDragonfly extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'dragonfly_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -122,7 +133,7 @@ class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -54,6 +54,17 @@ class StandaloneKeydb extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'keydb_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -123,7 +134,7 @@ class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->keydb_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -57,6 +57,18 @@ class StandaloneMariadb extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'mariadb_password',
|
||||
'mariadb_root_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -126,7 +138,7 @@ class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mariadb_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -57,6 +57,17 @@ class StandaloneMongodb extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'mongo_initdb_root_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -132,7 +143,7 @@ class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mongo_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -58,6 +58,18 @@ class StandaloneMysql extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'mysql_password',
|
||||
'mysql_root_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -128,7 +140,7 @@ class StandaloneMysql extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mysql_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -60,6 +60,18 @@ class StandalonePostgresql extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'postgres_password',
|
||||
'init_scripts',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -170,7 +182,7 @@ class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -53,6 +53,17 @@ class StandaloneRedis extends BaseModel
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
/**
|
||||
* Sensitive fields hidden by default in serialized output (toArray/toJson).
|
||||
* API controllers should call makeVisible([...]) for callers with the
|
||||
* `read:sensitive` or `root` token ability.
|
||||
*/
|
||||
protected $hidden = [
|
||||
'redis_password',
|
||||
'internal_db_url',
|
||||
'external_db_url',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
@@ -127,7 +138,7 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->redis_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
if ($oldConfigHash === null) {
|
||||
|
||||
@@ -124,6 +124,6 @@ class SwarmDocker extends BaseModel
|
||||
|
||||
public function attachedTo()
|
||||
{
|
||||
return $this->applications?->count() > 0 || $this->databases()->count() > 0;
|
||||
return $this->applications()->exists() || $this->databases()->count() > 0 || $this->services()->exists();
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -47,10 +47,12 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
'personal_team',
|
||||
'show_boarding',
|
||||
'custom_server_limit',
|
||||
'is_mcp_server_enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'personal_team' => 'boolean',
|
||||
'is_mcp_server_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
@@ -66,7 +68,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
$team->webhookNotificationSettings()->create();
|
||||
});
|
||||
|
||||
static::saving(function ($team) {
|
||||
static::updating(function ($team) {
|
||||
if (auth()->user()?->isMember()) {
|
||||
throw new \Exception('You are not allowed to update this team.');
|
||||
}
|
||||
|
||||
@@ -49,6 +49,25 @@ class TelegramNotificationSettings extends Model
|
||||
'telegram_notifications_traefik_outdated_thread_id',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'telegram_token',
|
||||
'telegram_chat_id',
|
||||
'telegram_notifications_deployment_success_thread_id',
|
||||
'telegram_notifications_deployment_failure_thread_id',
|
||||
'telegram_notifications_status_change_thread_id',
|
||||
'telegram_notifications_backup_success_thread_id',
|
||||
'telegram_notifications_backup_failure_thread_id',
|
||||
'telegram_notifications_scheduled_task_success_thread_id',
|
||||
'telegram_notifications_scheduled_task_failure_thread_id',
|
||||
'telegram_notifications_docker_cleanup_success_thread_id',
|
||||
'telegram_notifications_docker_cleanup_failure_thread_id',
|
||||
'telegram_notifications_server_disk_usage_thread_id',
|
||||
'telegram_notifications_server_reachable_thread_id',
|
||||
'telegram_notifications_server_unreachable_thread_id',
|
||||
'telegram_notifications_server_patch_thread_id',
|
||||
'telegram_notifications_traefik_outdated_thread_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'telegram_enabled' => 'boolean',
|
||||
'telegram_token' => 'encrypted',
|
||||
@@ -75,7 +94,8 @@ class TelegramNotificationSettings extends Model
|
||||
'telegram_notifications_backup_failure_thread_id' => 'encrypted',
|
||||
'telegram_notifications_scheduled_task_success_thread_id' => 'encrypted',
|
||||
'telegram_notifications_scheduled_task_failure_thread_id' => 'encrypted',
|
||||
'telegram_notifications_docker_cleanup_thread_id' => 'encrypted',
|
||||
'telegram_notifications_docker_cleanup_success_thread_id' => 'encrypted',
|
||||
'telegram_notifications_docker_cleanup_failure_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_disk_usage_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_reachable_thread_id' => 'encrypted',
|
||||
'telegram_notifications_server_unreachable_thread_id' => 'encrypted',
|
||||
|
||||
@@ -349,6 +349,11 @@ class User extends Authenticatable implements SendsEmail
|
||||
{
|
||||
$sessionTeamId = data_get(session('currentTeam'), 'id');
|
||||
|
||||
// Fallback for stateless API requests: resolve team from Sanctum token
|
||||
if (is_null($sessionTeamId) && $this->currentAccessToken()) {
|
||||
$sessionTeamId = data_get($this->currentAccessToken(), 'team_id');
|
||||
}
|
||||
|
||||
if (is_null($sessionTeamId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ class WebhookNotificationSettings extends Model
|
||||
'traefik_outdated_webhook_notifications',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'webhook_url',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Reference in new issue
Block a user