Merge branch 'next' into shadow/fix-typo-slash-proxy-page
This commit is contained in:
35
app/Events/ApplicationConfigurationChanged.php
Normal file
35
app/Events/ApplicationConfigurationChanged.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ApplicationConfigurationChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ namespace App\Jobs;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ApplicationConfigurationChanged;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
@@ -147,6 +148,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private Collection $saved_outputs;
|
||||
|
||||
private ?string $secrets_hash_key = null;
|
||||
|
||||
private ?string $full_healthcheck_url = null;
|
||||
|
||||
private string $serverUser = 'root';
|
||||
@@ -606,6 +609,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
|
||||
// Modify Dockerfiles for ARGs and build secrets
|
||||
$this->modify_dockerfiles_for_compose($composeFile);
|
||||
// Build new container to limit downtime.
|
||||
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
|
||||
|
||||
@@ -632,6 +638,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
} else {
|
||||
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
|
||||
}
|
||||
|
||||
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
|
||||
$build_args_string = $this->build_args->implode(' ');
|
||||
$command .= " {$build_args_string}";
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
|
||||
}
|
||||
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
|
||||
);
|
||||
@@ -2702,22 +2715,28 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$variables = collect($this->nixpacks_plan_json->get('variables'));
|
||||
} else {
|
||||
// Generate environment variables for build process (filters by is_buildtime = true)
|
||||
$this->generate_env_variables();
|
||||
$variables = collect([])->merge($this->env_args);
|
||||
}
|
||||
|
||||
// Check if build secrets are enabled and BuildKit is supported
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
$this->generate_build_secrets($variables);
|
||||
$this->build_args = '';
|
||||
} else {
|
||||
// Fall back to traditional build args
|
||||
$secrets_hash = '';
|
||||
if ($variables->isNotEmpty()) {
|
||||
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||
}
|
||||
|
||||
$this->build_args = $variables->map(function ($value, $key) {
|
||||
$value = escapeshellarg($value);
|
||||
|
||||
return "--build-arg {$key}={$value}";
|
||||
});
|
||||
|
||||
if ($secrets_hash) {
|
||||
$this->build_args->push("--build-arg COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2736,13 +2755,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
return '';
|
||||
}
|
||||
|
||||
return $variables
|
||||
$secrets_hash = $this->generate_secrets_hash($variables);
|
||||
$env_flags = $variables
|
||||
->map(function ($env) {
|
||||
$escaped_value = escapeshellarg($env->real_value);
|
||||
|
||||
return "-e {$env->key}={$escaped_value}";
|
||||
})
|
||||
->implode(' ');
|
||||
|
||||
$env_flags .= " -e COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}";
|
||||
|
||||
return $env_flags;
|
||||
}
|
||||
|
||||
private function generate_build_secrets(Collection $variables)
|
||||
@@ -2758,6 +2782,36 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
return "--secret id={$key},env={$key}";
|
||||
})
|
||||
->implode(' ');
|
||||
|
||||
$this->build_secrets .= ' --secret id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
|
||||
}
|
||||
|
||||
private function generate_secrets_hash($variables)
|
||||
{
|
||||
if (! $this->secrets_hash_key) {
|
||||
$this->secrets_hash_key = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
if ($variables instanceof Collection) {
|
||||
$secrets_string = $variables
|
||||
->mapWithKeys(function ($value, $key) {
|
||||
return [$key => $value];
|
||||
})
|
||||
->sortKeys()
|
||||
->map(function ($value, $key) {
|
||||
return "{$key}={$value}";
|
||||
})
|
||||
->implode('|');
|
||||
} else {
|
||||
$secrets_string = $variables
|
||||
->map(function ($env) {
|
||||
return "{$env->key}={$env->real_value}";
|
||||
})
|
||||
->sort()
|
||||
->implode('|');
|
||||
}
|
||||
|
||||
return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key);
|
||||
}
|
||||
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
@@ -2799,6 +2853,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($envs->isNotEmpty()) {
|
||||
$secrets_hash = $this->generate_secrets_hash($envs);
|
||||
$dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]);
|
||||
}
|
||||
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}{$this->dockerfile_location} > /dev/null"),
|
||||
@@ -2830,8 +2890,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
// Get environment variables for secrets
|
||||
$variables = $this->pull_request_id === 0
|
||||
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get()
|
||||
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
|
||||
? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
|
||||
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
|
||||
|
||||
if ($variables->isEmpty()) {
|
||||
return;
|
||||
@@ -2840,6 +2900,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
// Generate mount strings for all secrets
|
||||
$mountStrings = $variables->map(fn ($env) => "--mount=type=secret,id={$env->key},env={$env->key}")->implode(' ');
|
||||
|
||||
// Add mount for the secrets hash to ensure cache invalidation
|
||||
$mountStrings .= ' --mount=type=secret,id=COOLIFY_BUILD_SECRETS_HASH,env=COOLIFY_BUILD_SECRETS_HASH';
|
||||
|
||||
$modified = false;
|
||||
$dockerfile = $dockerfile->map(function ($line) use ($mountStrings, &$modified) {
|
||||
$trimmed = ltrim($line);
|
||||
@@ -2868,6 +2931,164 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
}
|
||||
|
||||
private function modify_dockerfiles_for_compose($composeFile)
|
||||
{
|
||||
if ($this->application->build_pack !== 'dockercompose') {
|
||||
return;
|
||||
}
|
||||
|
||||
$variables = $this->pull_request_id === 0
|
||||
? $this->application->environment_variables()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get()
|
||||
: $this->application->environment_variables_preview()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
if ($variables->isEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$services = data_get($composeFile, 'services', []);
|
||||
|
||||
foreach ($services as $serviceName => $service) {
|
||||
if (! isset($service['build'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$context = '.';
|
||||
$dockerfile = 'Dockerfile';
|
||||
|
||||
if (is_string($service['build'])) {
|
||||
$context = $service['build'];
|
||||
} elseif (is_array($service['build'])) {
|
||||
$context = data_get($service['build'], 'context', '.');
|
||||
$dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile');
|
||||
}
|
||||
|
||||
$dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/');
|
||||
if (str_starts_with($dockerfilePath, './')) {
|
||||
$dockerfilePath = substr($dockerfilePath, 2);
|
||||
}
|
||||
if (str_starts_with($dockerfilePath, '/')) {
|
||||
$dockerfilePath = substr($dockerfilePath, 1);
|
||||
}
|
||||
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"),
|
||||
'hidden' => true,
|
||||
'save' => 'dockerfile_check_'.$serviceName,
|
||||
]);
|
||||
|
||||
if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') {
|
||||
$this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"),
|
||||
'hidden' => true,
|
||||
'save' => 'dockerfile_content_'.$serviceName,
|
||||
]);
|
||||
|
||||
$dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName);
|
||||
if (! $dockerfileContent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n"));
|
||||
|
||||
$fromIndices = [];
|
||||
$dockerfile_lines->each(function ($line, $index) use (&$fromIndices) {
|
||||
if (str($line)->trim()->startsWith('FROM')) {
|
||||
$fromIndices[] = $index;
|
||||
}
|
||||
});
|
||||
|
||||
if (empty($fromIndices)) {
|
||||
$this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$isMultiStage = count($fromIndices) > 1;
|
||||
|
||||
$argsToAdd = collect([]);
|
||||
foreach ($variables as $env) {
|
||||
$argsToAdd->push("ARG {$env->key}");
|
||||
}
|
||||
|
||||
ray($argsToAdd);
|
||||
if ($argsToAdd->isEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add.");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalAdded = 0;
|
||||
$offset = 0;
|
||||
|
||||
foreach ($fromIndices as $stageIndex => $fromIndex) {
|
||||
$adjustedIndex = $fromIndex + $offset;
|
||||
|
||||
$stageStart = $adjustedIndex + 1;
|
||||
$stageEnd = isset($fromIndices[$stageIndex + 1])
|
||||
? $fromIndices[$stageIndex + 1] + $offset
|
||||
: $dockerfile_lines->count();
|
||||
|
||||
$existingStageArgs = collect([]);
|
||||
for ($i = $stageStart; $i < $stageEnd; $i++) {
|
||||
$line = $dockerfile_lines->get($i);
|
||||
if (! $line || ! str($line)->trim()->startsWith('ARG')) {
|
||||
break;
|
||||
}
|
||||
$parts = explode(' ', trim($line), 2);
|
||||
if (count($parts) >= 2) {
|
||||
$argPart = $parts[1];
|
||||
$keyValue = explode('=', $argPart, 2);
|
||||
$existingStageArgs->push($keyValue[0]);
|
||||
}
|
||||
}
|
||||
|
||||
$stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) {
|
||||
$key = str($arg)->after('ARG ')->trim()->toString();
|
||||
|
||||
return ! $existingStageArgs->contains($key);
|
||||
});
|
||||
|
||||
if ($stageArgsToAdd->isNotEmpty()) {
|
||||
$dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray());
|
||||
$totalAdded += $stageArgsToAdd->count();
|
||||
$offset += $stageArgsToAdd->count();
|
||||
}
|
||||
}
|
||||
|
||||
if ($totalAdded > 0) {
|
||||
$dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
|
||||
$stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : '';
|
||||
$this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}.");
|
||||
} else {
|
||||
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
|
||||
}
|
||||
|
||||
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
|
||||
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
|
||||
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
|
||||
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function add_build_secrets_to_compose($composeFile)
|
||||
{
|
||||
// Get environment variables for secrets
|
||||
@@ -3018,6 +3239,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
queue_next_deployment($this->application);
|
||||
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
ray($this->application->team()->id);
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
|
372
app/Livewire/GlobalSearch.php
Normal file
372
app/Livewire/GlobalSearch.php
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class GlobalSearch extends Component
|
||||
{
|
||||
public $searchQuery = '';
|
||||
|
||||
public $isModalOpen = false;
|
||||
|
||||
public $searchResults = [];
|
||||
|
||||
public $allSearchableItems = [];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->searchQuery = '';
|
||||
$this->isModalOpen = false;
|
||||
$this->searchResults = [];
|
||||
$this->allSearchableItems = [];
|
||||
}
|
||||
|
||||
public function openSearchModal()
|
||||
{
|
||||
$this->isModalOpen = true;
|
||||
$this->loadSearchableItems();
|
||||
$this->dispatch('search-modal-opened');
|
||||
}
|
||||
|
||||
public function closeSearchModal()
|
||||
{
|
||||
$this->isModalOpen = false;
|
||||
$this->searchQuery = '';
|
||||
$this->searchResults = [];
|
||||
}
|
||||
|
||||
public static function getCacheKey($teamId)
|
||||
{
|
||||
return 'global_search_items_'.$teamId;
|
||||
}
|
||||
|
||||
public static function clearTeamCache($teamId)
|
||||
{
|
||||
Cache::forget(self::getCacheKey($teamId));
|
||||
}
|
||||
|
||||
public function updatedSearchQuery()
|
||||
{
|
||||
$this->search();
|
||||
}
|
||||
|
||||
private function loadSearchableItems()
|
||||
{
|
||||
// Try to get from Redis cache first
|
||||
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
|
||||
|
||||
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
|
||||
ray()->showQueries();
|
||||
$items = collect();
|
||||
$team = auth()->user()->currentTeam();
|
||||
|
||||
// Get all applications
|
||||
$applications = Application::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($app) {
|
||||
// Collect all FQDNs from the application
|
||||
$fqdns = collect([]);
|
||||
|
||||
// For regular applications
|
||||
if ($app->fqdn) {
|
||||
$fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
|
||||
}
|
||||
|
||||
// For docker compose based applications
|
||||
if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) {
|
||||
try {
|
||||
$composeDomains = json_decode($app->docker_compose_domains, true);
|
||||
if (is_array($composeDomains)) {
|
||||
foreach ($composeDomains as $serviceName => $domains) {
|
||||
if (is_array($domains)) {
|
||||
$fqdns = $fqdns->merge($domains);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignore JSON parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
$fqdnsString = $fqdns->implode(' ');
|
||||
|
||||
return [
|
||||
'id' => $app->id,
|
||||
'name' => $app->name,
|
||||
'type' => 'application',
|
||||
'uuid' => $app->uuid,
|
||||
'description' => $app->description,
|
||||
'link' => $app->link(),
|
||||
'project' => $app->environment->project->name ?? null,
|
||||
'environment' => $app->environment->name ?? null,
|
||||
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
|
||||
'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString),
|
||||
];
|
||||
});
|
||||
|
||||
// Get all services
|
||||
$services = Service::ownedByCurrentTeam()
|
||||
->with(['environment.project', 'applications'])
|
||||
->get()
|
||||
->map(function ($service) {
|
||||
// Collect all FQDNs from service applications
|
||||
$fqdns = collect([]);
|
||||
foreach ($service->applications as $app) {
|
||||
if ($app->fqdn) {
|
||||
$appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
|
||||
$fqdns = $fqdns->merge($appFqdns);
|
||||
}
|
||||
}
|
||||
$fqdnsString = $fqdns->implode(' ');
|
||||
|
||||
return [
|
||||
'id' => $service->id,
|
||||
'name' => $service->name,
|
||||
'type' => 'service',
|
||||
'uuid' => $service->uuid,
|
||||
'description' => $service->description,
|
||||
'link' => $service->link(),
|
||||
'project' => $service->environment->project->name ?? null,
|
||||
'environment' => $service->environment->name ?? null,
|
||||
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
|
||||
'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString),
|
||||
];
|
||||
});
|
||||
|
||||
// Get all standalone databases
|
||||
$databases = collect();
|
||||
|
||||
// PostgreSQL
|
||||
$databases = $databases->merge(
|
||||
StandalonePostgresql::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'postgresql',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' postgresql '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// MySQL
|
||||
$databases = $databases->merge(
|
||||
StandaloneMysql::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'mysql',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' mysql '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// MariaDB
|
||||
$databases = $databases->merge(
|
||||
StandaloneMariadb::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'mariadb',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' mariadb '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// MongoDB
|
||||
$databases = $databases->merge(
|
||||
StandaloneMongodb::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'mongodb',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' mongodb '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// Redis
|
||||
$databases = $databases->merge(
|
||||
StandaloneRedis::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'redis',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' redis '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// KeyDB
|
||||
$databases = $databases->merge(
|
||||
StandaloneKeydb::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'keydb',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' keydb '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// Dragonfly
|
||||
$databases = $databases->merge(
|
||||
StandaloneDragonfly::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'dragonfly',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' dragonfly '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// Clickhouse
|
||||
$databases = $databases->merge(
|
||||
StandaloneClickhouse::ownedByCurrentTeam()
|
||||
->with(['environment.project'])
|
||||
->get()
|
||||
->map(function ($db) {
|
||||
return [
|
||||
'id' => $db->id,
|
||||
'name' => $db->name,
|
||||
'type' => 'database',
|
||||
'subtype' => 'clickhouse',
|
||||
'uuid' => $db->uuid,
|
||||
'description' => $db->description,
|
||||
'link' => $db->link(),
|
||||
'project' => $db->environment->project->name ?? null,
|
||||
'environment' => $db->environment->name ?? null,
|
||||
'search_text' => strtolower($db->name.' clickhouse '.$db->description),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
// Get all servers
|
||||
$servers = Server::ownedByCurrentTeam()
|
||||
->get()
|
||||
->map(function ($server) {
|
||||
return [
|
||||
'id' => $server->id,
|
||||
'name' => $server->name,
|
||||
'type' => 'server',
|
||||
'uuid' => $server->uuid,
|
||||
'description' => $server->description,
|
||||
'link' => $server->url(),
|
||||
'project' => null,
|
||||
'environment' => null,
|
||||
'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description),
|
||||
];
|
||||
});
|
||||
|
||||
// Merge all collections
|
||||
$items = $items->merge($applications)
|
||||
->merge($services)
|
||||
->merge($databases)
|
||||
->merge($servers);
|
||||
|
||||
return $items->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
private function search()
|
||||
{
|
||||
if (strlen($this->searchQuery) < 2) {
|
||||
$this->searchResults = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query = strtolower($this->searchQuery);
|
||||
|
||||
// Case-insensitive search in the items
|
||||
$this->searchResults = collect($this->allSearchableItems)
|
||||
->filter(function ($item) use ($query) {
|
||||
return str_contains($item['search_text'], $query);
|
||||
})
|
||||
->take(20)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.global-search');
|
||||
}
|
||||
}
|
@@ -20,7 +20,15 @@ class ConfigurationChecker extends Component
|
||||
|
||||
public Application|Service|StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource;
|
||||
|
||||
protected $listeners = ['configurationChanged'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ApplicationConfigurationChanged" => 'configurationChanged',
|
||||
'configurationChanged' => 'configurationChanged',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
@@ -8,7 +8,7 @@ class Metrics extends Component
|
||||
{
|
||||
public $resource;
|
||||
|
||||
public $chartId = 'container-cpu';
|
||||
public $chartId = 'metrics';
|
||||
|
||||
public $data;
|
||||
|
||||
|
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Services\ConfigurationGenerator;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasConfiguration;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -110,7 +111,7 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Application extends BaseModel
|
||||
{
|
||||
use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '5';
|
||||
|
||||
@@ -123,66 +124,6 @@ class Application extends BaseModel
|
||||
'http_basic_auth_password' => 'encrypted',
|
||||
];
|
||||
|
||||
public function customNetworkAliases(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (is_null($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's already a JSON string, decode it
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
// If it's a string but not JSON, treat it as a comma-separated list
|
||||
if (is_string($value) && ! is_array($value)) {
|
||||
$value = explode(',', $value);
|
||||
}
|
||||
|
||||
$value = collect($value)
|
||||
->map(function ($alias) {
|
||||
if (is_string($alias)) {
|
||||
return str_replace(' ', '-', trim($alias));
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->filter()
|
||||
->unique() // Remove duplicate values
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return empty($value) ? null : json_encode($value);
|
||||
},
|
||||
get: function ($value) {
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
return json_decode($value, true);
|
||||
}
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid JSON
|
||||
*/
|
||||
private function isJson($string)
|
||||
{
|
||||
if (! is_string($string)) {
|
||||
return false;
|
||||
}
|
||||
json_decode($string);
|
||||
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::addGlobalScope('withRelations', function ($builder) {
|
||||
@@ -250,6 +191,66 @@ class Application extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public function customNetworkAliases(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: function ($value) {
|
||||
if (is_null($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it's already a JSON string, decode it
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
$value = json_decode($value, true);
|
||||
}
|
||||
|
||||
// If it's a string but not JSON, treat it as a comma-separated list
|
||||
if (is_string($value) && ! is_array($value)) {
|
||||
$value = explode(',', $value);
|
||||
}
|
||||
|
||||
$value = collect($value)
|
||||
->map(function ($alias) {
|
||||
if (is_string($alias)) {
|
||||
return str_replace(' ', '-', trim($alias));
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
->filter()
|
||||
->unique() // Remove duplicate values
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
return empty($value) ? null : json_encode($value);
|
||||
},
|
||||
get: function ($value) {
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($value) && $this->isJson($value)) {
|
||||
return json_decode($value, true);
|
||||
}
|
||||
|
||||
return is_array($value) ? $value : [];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid JSON
|
||||
*/
|
||||
private function isJson($string)
|
||||
{
|
||||
if (! is_string($string)) {
|
||||
return false;
|
||||
}
|
||||
json_decode($string);
|
||||
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||
|
@@ -85,6 +85,47 @@ class ApplicationDeploymentQueue extends Model
|
||||
return str($this->commit_message)->value();
|
||||
}
|
||||
|
||||
private function redactSensitiveInfo($text)
|
||||
{
|
||||
$text = remove_iip($text);
|
||||
|
||||
$app = $this->application;
|
||||
if (! $app) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$lockedVars = collect([]);
|
||||
|
||||
if ($app->environment_variables) {
|
||||
$lockedVars = $lockedVars->merge(
|
||||
$app->environment_variables
|
||||
->where('is_shown_once', true)
|
||||
->pluck('real_value', 'key')
|
||||
->filter()
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->pull_request_id !== 0 && $app->environment_variables_preview) {
|
||||
$lockedVars = $lockedVars->merge(
|
||||
$app->environment_variables_preview
|
||||
->where('is_shown_once', true)
|
||||
->pluck('real_value', 'key')
|
||||
->filter()
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($lockedVars as $key => $value) {
|
||||
$escapedValue = preg_quote($value, '/');
|
||||
$text = preg_replace(
|
||||
'/'.$escapedValue.'/',
|
||||
REDACTED,
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
|
||||
{
|
||||
if ($type === 'error') {
|
||||
@@ -96,7 +137,7 @@ class ApplicationDeploymentQueue extends Model
|
||||
}
|
||||
$newLogEntry = [
|
||||
'command' => null,
|
||||
'output' => remove_iip($message),
|
||||
'output' => $this->redactSensitiveInfo($message),
|
||||
'type' => $type,
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
|
@@ -13,6 +13,7 @@ use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Notifications\Server\Reachable;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -55,7 +56,7 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -41,7 +42,7 @@ use Visus\Cuid2\Cuid2;
|
||||
)]
|
||||
class Service extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '5';
|
||||
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -43,6 +44,11 @@ class StandaloneClickhouse extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -43,6 +44,11 @@ class StandaloneDragonfly extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -43,6 +44,11 @@ class StandaloneKeydb extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -44,6 +45,11 @@ class StandaloneMariadb extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -46,6 +47,11 @@ class StandaloneMongodb extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMysql extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -44,6 +45,11 @@ class StandaloneMysql extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -44,6 +45,11 @@ class StandalonePostgresql extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
{
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneRedis extends BaseModel
|
||||
{
|
||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -45,6 +46,11 @@ class StandaloneRedis extends BaseModel
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
81
app/Traits/ClearsGlobalSearchCache.php
Normal file
81
app/Traits/ClearsGlobalSearchCache.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Livewire\GlobalSearch;
|
||||
|
||||
trait ClearsGlobalSearchCache
|
||||
{
|
||||
protected static function bootClearsGlobalSearchCache()
|
||||
{
|
||||
static::saving(function ($model) {
|
||||
// Only clear cache if searchable fields are being changed
|
||||
if ($model->hasSearchableChanges()) {
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static::created(function ($model) {
|
||||
// Always clear cache when model is created
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function ($model) {
|
||||
// Always clear cache when model is deleted
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function hasSearchableChanges(): bool
|
||||
{
|
||||
// Define searchable fields based on model type
|
||||
$searchableFields = ['name', 'description'];
|
||||
|
||||
// Add model-specific searchable fields
|
||||
if ($this instanceof \App\Models\Application) {
|
||||
$searchableFields[] = 'fqdn';
|
||||
$searchableFields[] = 'docker_compose_domains';
|
||||
} elseif ($this instanceof \App\Models\Server) {
|
||||
$searchableFields[] = 'ip';
|
||||
} elseif ($this instanceof \App\Models\Service) {
|
||||
// Services don't have direct fqdn, but name and description are covered
|
||||
}
|
||||
// Database models only have name and description as searchable
|
||||
|
||||
// Check if any searchable field is dirty
|
||||
foreach ($searchableFields as $field) {
|
||||
if ($this->isDirty($field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function getTeamIdForCache()
|
||||
{
|
||||
// For database models, team is accessed through environment.project.team
|
||||
if (method_exists($this, 'team')) {
|
||||
$team = $this->team();
|
||||
if (filled($team)) {
|
||||
return is_object($team) ? $team->id : null;
|
||||
}
|
||||
}
|
||||
|
||||
// For models with direct team_id property
|
||||
if (property_exists($this, 'team_id') || isset($this->team_id)) {
|
||||
return $this->team_id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@@ -17,6 +17,46 @@ trait ExecuteRemoteCommand
|
||||
|
||||
public static int $batch_counter = 0;
|
||||
|
||||
private function redact_sensitive_info($text)
|
||||
{
|
||||
$text = remove_iip($text);
|
||||
|
||||
if (! isset($this->application)) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$lockedVars = collect([]);
|
||||
|
||||
if (isset($this->application->environment_variables)) {
|
||||
$lockedVars = $lockedVars->merge(
|
||||
$this->application->environment_variables
|
||||
->where('is_shown_once', true)
|
||||
->pluck('real_value', 'key')
|
||||
->filter()
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->pull_request_id) && $this->pull_request_id !== 0 && isset($this->application->environment_variables_preview)) {
|
||||
$lockedVars = $lockedVars->merge(
|
||||
$this->application->environment_variables_preview
|
||||
->where('is_shown_once', true)
|
||||
->pluck('real_value', 'key')
|
||||
->filter()
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($lockedVars as $key => $value) {
|
||||
$escapedValue = preg_quote($value, '/');
|
||||
$text = preg_replace(
|
||||
'/'.$escapedValue.'/',
|
||||
REDACTED,
|
||||
$text
|
||||
);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function execute_remote_command(...$commands)
|
||||
{
|
||||
static::$batch_counter++;
|
||||
@@ -74,7 +114,7 @@ trait ExecuteRemoteCommand
|
||||
// Track SSH retry event in Sentry
|
||||
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
|
||||
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
|
||||
'command' => remove_iip($command),
|
||||
'command' => $this->redact_sensitive_info($command),
|
||||
'trait' => 'ExecuteRemoteCommand',
|
||||
]);
|
||||
|
||||
@@ -125,8 +165,8 @@ trait ExecuteRemoteCommand
|
||||
$sanitized_output = sanitize_utf8_text($output);
|
||||
|
||||
$new_log_entry = [
|
||||
'command' => remove_iip($command),
|
||||
'output' => remove_iip($sanitized_output),
|
||||
'command' => $this->redact_sensitive_info($command),
|
||||
'output' => $this->redact_sensitive_info($sanitized_output),
|
||||
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => $hidden,
|
||||
@@ -194,7 +234,7 @@ trait ExecuteRemoteCommand
|
||||
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
|
||||
|
||||
$new_log_entry = [
|
||||
'output' => remove_iip($retryMessage),
|
||||
'output' => $this->redact_sensitive_info($retryMessage),
|
||||
'type' => 'stdout',
|
||||
'timestamp' => Carbon::now('UTC'),
|
||||
'hidden' => false,
|
||||
|
Reference in New Issue
Block a user