mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 06:16:04 +00:00
Merge branch 'next' into v4.x
This commit is contained in:
31 files changed
+1620
-245
No files matched your search
@@ -96,7 +96,11 @@ class GetContainersStatus
|
||||
}
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
if ($containerStatus === 'restarting') {
|
||||
$containerStatus = "restarting ($containerHealth)";
|
||||
} else {
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
}
|
||||
$labels = Arr::undot(format_docker_labels_to_json($labels));
|
||||
$applicationId = data_get($labels, 'coolify.applicationId');
|
||||
if ($applicationId) {
|
||||
@@ -386,19 +390,33 @@ class GetContainersStatus
|
||||
return null;
|
||||
}
|
||||
|
||||
// Aggregate status: if any container is running, app is running
|
||||
$hasRunning = false;
|
||||
$hasRestarting = false;
|
||||
$hasUnhealthy = false;
|
||||
$hasExited = false;
|
||||
|
||||
foreach ($relevantStatuses as $status) {
|
||||
if (str($status)->contains('running')) {
|
||||
if (str($status)->contains('restarting')) {
|
||||
$hasRestarting = true;
|
||||
} elseif (str($status)->contains('running')) {
|
||||
$hasRunning = true;
|
||||
if (str($status)->contains('unhealthy')) {
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
} elseif (str($status)->contains('exited')) {
|
||||
$hasExited = true;
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasRestarting) {
|
||||
return 'degraded (unhealthy)';
|
||||
}
|
||||
|
||||
if ($hasRunning && $hasExited) {
|
||||
return 'degraded (unhealthy)';
|
||||
}
|
||||
|
||||
if ($hasRunning) {
|
||||
return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
|
||||
}
|
||||
|
||||
@@ -26,22 +26,22 @@ class ComplexStatusCheck
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
|
||||
$container = format_docker_command_output_to_json($container);
|
||||
if ($container->count() === 1) {
|
||||
$container = $container->first();
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
$containers = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
|
||||
$containers = format_docker_command_output_to_json($containers);
|
||||
|
||||
if ($containers->count() > 0) {
|
||||
$statusToSet = $this->aggregateContainerStatuses($application, $containers);
|
||||
|
||||
if ($is_main_server) {
|
||||
$statusFromDb = $application->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$application->update(['status' => "$containerStatus:$containerHealth"]);
|
||||
if ($statusFromDb !== $statusToSet) {
|
||||
$application->update(['status' => $statusToSet]);
|
||||
}
|
||||
} else {
|
||||
$additional_server = $application->additional_servers()->wherePivot('server_id', $server->id);
|
||||
$statusFromDb = $additional_server->first()->pivot->status;
|
||||
if ($statusFromDb !== $containerStatus) {
|
||||
$additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]);
|
||||
if ($statusFromDb !== $statusToSet) {
|
||||
$additional_server->updateExistingPivot($server->id, ['status' => $statusToSet]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -57,4 +57,78 @@ class ComplexStatusCheck
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function aggregateContainerStatuses($application, $containers)
|
||||
{
|
||||
$dockerComposeRaw = data_get($application, 'docker_compose_raw');
|
||||
$excludedContainers = collect();
|
||||
|
||||
if ($dockerComposeRaw) {
|
||||
try {
|
||||
$dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
|
||||
$services = data_get($dockerCompose, 'services', []);
|
||||
|
||||
foreach ($services as $serviceName => $serviceConfig) {
|
||||
$excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
|
||||
$restartPolicy = data_get($serviceConfig, 'restart', 'always');
|
||||
|
||||
if ($excludeFromHc || $restartPolicy === 'no') {
|
||||
$excludedContainers->push($serviceName);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// If we can't parse, treat all containers as included
|
||||
}
|
||||
}
|
||||
|
||||
$hasRunning = false;
|
||||
$hasRestarting = false;
|
||||
$hasUnhealthy = false;
|
||||
$hasExited = false;
|
||||
$relevantContainerCount = 0;
|
||||
|
||||
foreach ($containers as $container) {
|
||||
$labels = data_get($container, 'Config.Labels', []);
|
||||
$serviceName = data_get($labels, 'com.docker.compose.service');
|
||||
|
||||
if ($serviceName && $excludedContainers->contains($serviceName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relevantContainerCount++;
|
||||
$containerStatus = data_get($container, 'State.Status');
|
||||
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
|
||||
|
||||
if ($containerStatus === 'restarting') {
|
||||
$hasRestarting = true;
|
||||
$hasUnhealthy = true;
|
||||
} elseif ($containerStatus === 'running') {
|
||||
$hasRunning = true;
|
||||
if ($containerHealth === 'unhealthy') {
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
} elseif ($containerStatus === 'exited') {
|
||||
$hasExited = true;
|
||||
$hasUnhealthy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($relevantContainerCount === 0) {
|
||||
return 'running:healthy';
|
||||
}
|
||||
|
||||
if ($hasRestarting) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
if ($hasRunning && $hasExited) {
|
||||
return 'degraded:unhealthy';
|
||||
}
|
||||
|
||||
if ($hasRunning) {
|
||||
return $hasUnhealthy ? 'running:unhealthy' : 'running:healthy';
|
||||
}
|
||||
|
||||
return 'exited:unhealthy';
|
||||
}
|
||||
}
|
||||
@@ -2532,8 +2532,11 @@ class ApplicationsController extends Controller
|
||||
if ($env->is_shown_once != $request->is_shown_once) {
|
||||
$env->is_shown_once = $request->is_shown_once;
|
||||
}
|
||||
if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) {
|
||||
$env->is_buildtime_only = $request->is_buildtime_only;
|
||||
if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) {
|
||||
$env->is_runtime = $request->is_runtime;
|
||||
}
|
||||
if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
|
||||
$env->is_buildtime = $request->is_buildtime;
|
||||
}
|
||||
$env->save();
|
||||
|
||||
@@ -2559,8 +2562,11 @@ class ApplicationsController extends Controller
|
||||
if ($env->is_shown_once != $request->is_shown_once) {
|
||||
$env->is_shown_once = $request->is_shown_once;
|
||||
}
|
||||
if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) {
|
||||
$env->is_buildtime_only = $request->is_buildtime_only;
|
||||
if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) {
|
||||
$env->is_runtime = $request->is_runtime;
|
||||
}
|
||||
if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
|
||||
$env->is_buildtime = $request->is_buildtime;
|
||||
}
|
||||
$env->save();
|
||||
|
||||
@@ -2723,8 +2729,11 @@ class ApplicationsController extends Controller
|
||||
if ($env->is_shown_once != $item->get('is_shown_once')) {
|
||||
$env->is_shown_once = $item->get('is_shown_once');
|
||||
}
|
||||
if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) {
|
||||
$env->is_buildtime_only = $item->get('is_buildtime_only');
|
||||
if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) {
|
||||
$env->is_runtime = $item->get('is_runtime');
|
||||
}
|
||||
if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
|
||||
$env->is_buildtime = $item->get('is_buildtime');
|
||||
}
|
||||
$env->save();
|
||||
} else {
|
||||
@@ -2735,7 +2744,8 @@ class ApplicationsController extends Controller
|
||||
'is_literal' => $is_literal,
|
||||
'is_multiline' => $is_multi_line,
|
||||
'is_shown_once' => $is_shown_once,
|
||||
'is_buildtime_only' => $item->get('is_buildtime_only', false),
|
||||
'is_runtime' => $item->get('is_runtime', true),
|
||||
'is_buildtime' => $item->get('is_buildtime', true),
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -2753,8 +2763,11 @@ class ApplicationsController extends Controller
|
||||
if ($env->is_shown_once != $item->get('is_shown_once')) {
|
||||
$env->is_shown_once = $item->get('is_shown_once');
|
||||
}
|
||||
if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) {
|
||||
$env->is_buildtime_only = $item->get('is_buildtime_only');
|
||||
if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) {
|
||||
$env->is_runtime = $item->get('is_runtime');
|
||||
}
|
||||
if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
|
||||
$env->is_buildtime = $item->get('is_buildtime');
|
||||
}
|
||||
$env->save();
|
||||
} else {
|
||||
@@ -2765,7 +2778,8 @@ class ApplicationsController extends Controller
|
||||
'is_literal' => $is_literal,
|
||||
'is_multiline' => $is_multi_line,
|
||||
'is_shown_once' => $is_shown_once,
|
||||
'is_buildtime_only' => $item->get('is_buildtime_only', false),
|
||||
'is_runtime' => $item->get('is_runtime', true),
|
||||
'is_buildtime' => $item->get('is_buildtime', true),
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -2904,7 +2918,8 @@ class ApplicationsController extends Controller
|
||||
'is_literal' => $request->is_literal ?? false,
|
||||
'is_multiline' => $request->is_multiline ?? false,
|
||||
'is_shown_once' => $request->is_shown_once ?? false,
|
||||
'is_buildtime_only' => $request->is_buildtime_only ?? false,
|
||||
'is_runtime' => $request->is_runtime ?? true,
|
||||
'is_buildtime' => $request->is_buildtime ?? true,
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
@@ -2927,7 +2942,8 @@ class ApplicationsController extends Controller
|
||||
'is_literal' => $request->is_literal ?? false,
|
||||
'is_multiline' => $request->is_multiline ?? false,
|
||||
'is_shown_once' => $request->is_shown_once ?? false,
|
||||
'is_buildtime_only' => $request->is_buildtime_only ?? false,
|
||||
'is_runtime' => $request->is_runtime ?? true,
|
||||
'is_buildtime' => $request->is_buildtime ?? true,
|
||||
'resourceable_type' => get_class($application),
|
||||
'resourceable_id' => $application->id,
|
||||
]);
|
||||
|
||||
@@ -169,6 +169,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
private bool $dockerBuildkitSupported = false;
|
||||
|
||||
private bool $skip_build = false;
|
||||
|
||||
private Collection|string $build_secrets;
|
||||
|
||||
public function tags()
|
||||
@@ -566,7 +568,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->application->settings->is_raw_compose_deployment_enabled) {
|
||||
$this->application->oldRawParser();
|
||||
$yaml = $composeFile = $this->application->docker_compose_raw;
|
||||
$this->save_environment_variables();
|
||||
$this->generate_runtime_environment_variables();
|
||||
|
||||
// For raw compose, we cannot automatically add secrets configuration
|
||||
// User must define it manually in their docker-compose file
|
||||
@@ -575,7 +577,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
} else {
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$this->save_environment_variables();
|
||||
$this->generate_runtime_environment_variables();
|
||||
if (filled($this->env_filename)) {
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
@@ -604,6 +606,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.');
|
||||
|
||||
@@ -630,6 +635,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],
|
||||
);
|
||||
@@ -759,6 +771,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->generate_compose_file();
|
||||
$this->generate_build_env_variables();
|
||||
$this->build_image();
|
||||
|
||||
// For Nixpacks, save runtime environment variables AFTER the build
|
||||
// to prevent them from being accessible during the build process
|
||||
$this->save_runtime_environment_variables();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
@@ -952,18 +968,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
|
||||
if ($this->is_this_additional_server) {
|
||||
$this->skip_build = true;
|
||||
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->generate_compose_file();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
if ($this->restart_only) {
|
||||
$this->post_deployment();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
if (! $this->application->isConfigurationChanged()) {
|
||||
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
|
||||
$this->skip_build = true;
|
||||
$this->generate_compose_file();
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
@@ -1004,7 +1019,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function save_environment_variables()
|
||||
private function generate_runtime_environment_variables()
|
||||
{
|
||||
$envs = collect([]);
|
||||
$sort = $this->application->settings->is_env_sorting_enabled;
|
||||
@@ -1061,9 +1076,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out buildtime-only variables from runtime environment
|
||||
// Filter runtime variables (only include variables that are available at runtime)
|
||||
$runtime_environment_variables = $sorted_environment_variables->filter(function ($env) {
|
||||
return ! $env->is_buildtime_only;
|
||||
return $env->is_runtime;
|
||||
});
|
||||
|
||||
// Sort runtime environment variables: those referencing SERVICE_ variables come after others
|
||||
@@ -1117,9 +1132,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out buildtime-only variables from runtime environment for preview
|
||||
// Filter runtime variables for preview (only include variables that are available at runtime)
|
||||
$runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) {
|
||||
return ! $env->is_buildtime_only;
|
||||
return $env->is_runtime;
|
||||
});
|
||||
|
||||
// Sort runtime environment variables: those referencing SERVICE_ variables come after others
|
||||
@@ -1176,13 +1191,53 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
$this->env_filename = null;
|
||||
} else {
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
// For Nixpacks builds, we save the .env file AFTER the build to prevent
|
||||
// runtime-only variables from being accessible during the build process
|
||||
if ($this->application->build_pack !== 'nixpacks' || $this->skip_build) {
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"),
|
||||
],
|
||||
|
||||
);
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->original_server;
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
|
||||
]
|
||||
);
|
||||
$this->server = $this->build_server;
|
||||
} else {
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->environment_variables = $envs;
|
||||
}
|
||||
|
||||
private function save_runtime_environment_variables()
|
||||
{
|
||||
// This method saves the .env file with runtime variables
|
||||
// It should be called AFTER the build for Nixpacks to prevent runtime-only variables
|
||||
// from being accessible during the build process
|
||||
|
||||
if ($this->environment_variables && $this->environment_variables->isNotEmpty() && $this->env_filename) {
|
||||
$envs_base64 = base64_encode($this->environment_variables->implode("\n"));
|
||||
|
||||
// Write .env file to workdir (for container runtime)
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"),
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
// Write .env file to configuration directory
|
||||
if ($this->use_build_server) {
|
||||
$this->server = $this->original_server;
|
||||
$this->execute_remote_command(
|
||||
@@ -1199,7 +1254,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->environment_variables = $envs;
|
||||
}
|
||||
|
||||
private function elixir_finetunes()
|
||||
@@ -1418,6 +1472,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->add_build_env_variables_to_dockerfile();
|
||||
}
|
||||
$this->build_image();
|
||||
// For Nixpacks, save runtime environment variables AFTER the build
|
||||
if ($this->application->build_pack === 'nixpacks') {
|
||||
$this->save_runtime_environment_variables();
|
||||
}
|
||||
$this->push_to_docker_registry();
|
||||
$this->rolling_update();
|
||||
}
|
||||
@@ -1681,6 +1739,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
$nixpacks_command = $this->nixpacks_build_cmd();
|
||||
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
|
||||
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true],
|
||||
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true],
|
||||
@@ -1700,6 +1759,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$parsed = Toml::Parse($this->nixpacks_plan);
|
||||
|
||||
// Do any modifications here
|
||||
// We need to generate envs here because nixpacks need to know to generate a proper Dockerfile
|
||||
$this->generate_env_variables();
|
||||
$merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args);
|
||||
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
|
||||
@@ -1872,13 +1932,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->env_args->put('SOURCE_COMMIT', $this->commit);
|
||||
$coolify_envs = $this->generate_coolify_env_variables();
|
||||
|
||||
// Include ALL environment variables (both build-time and runtime) for all build packs
|
||||
// This deprecates the need for is_build_time flag
|
||||
// For build process, include only environment variables where is_buildtime = true
|
||||
if ($this->pull_request_id === 0) {
|
||||
// Get all environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds
|
||||
$envs = $this->application->build_pack === 'nixpacks'
|
||||
? $this->application->runtime_environment_variables
|
||||
: $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get();
|
||||
// Get environment variables that are marked as available during build
|
||||
$envs = $this->application->environment_variables()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
foreach ($envs as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
@@ -1900,10 +1960,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get all preview environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds
|
||||
$envs = $this->application->build_pack === 'nixpacks'
|
||||
? $this->application->runtime_environment_variables_preview
|
||||
: $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
|
||||
// Get preview environment variables that are marked as available during build
|
||||
$envs = $this->application->environment_variables_preview()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
foreach ($envs as $env) {
|
||||
if (! is_null($env->real_value)) {
|
||||
@@ -1935,8 +1996,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->application->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
// $environment_variables = $this->generate_environment_variables($ports);
|
||||
$this->save_environment_variables();
|
||||
$this->generate_runtime_environment_variables();
|
||||
if (data_get($this->application, 'custom_labels')) {
|
||||
$this->application->parseContainerLabels();
|
||||
$labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels)));
|
||||
@@ -2652,6 +2712,7 @@ 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);
|
||||
}
|
||||
@@ -2678,8 +2739,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
|
||||
$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 '';
|
||||
@@ -2722,7 +2783,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
|
||||
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get();
|
||||
// Only add environment variables that are available during build
|
||||
$envs = $this->application->environment_variables()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
foreach ($envs as $env) {
|
||||
if (data_get($env, 'is_multiline') === true) {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
|
||||
@@ -2731,8 +2796,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get all preview environment variables except NIXPACKS_ prefixed ones
|
||||
$envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
|
||||
// Only add preview environment variables that are available during build
|
||||
$envs = $this->application->environment_variables_preview()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
foreach ($envs as $env) {
|
||||
if (data_get($env, 'is_multiline') === true) {
|
||||
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
|
||||
@@ -2772,8 +2840,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;
|
||||
@@ -2810,6 +2878,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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ class Add extends Component
|
||||
|
||||
public bool $is_literal = false;
|
||||
|
||||
public bool $is_buildtime_only = false;
|
||||
public bool $is_runtime = true;
|
||||
|
||||
public bool $is_buildtime = true;
|
||||
|
||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||
|
||||
@@ -32,7 +34,8 @@ class Add extends Component
|
||||
'value' => 'nullable',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_buildtime_only' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -40,7 +43,8 @@ class Add extends Component
|
||||
'value' => 'value',
|
||||
'is_multiline' => 'multiline',
|
||||
'is_literal' => 'literal',
|
||||
'is_buildtime_only' => 'buildtime only',
|
||||
'is_runtime' => 'runtime',
|
||||
'is_buildtime' => 'buildtime',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -56,7 +60,8 @@ class Add extends Component
|
||||
'value' => $this->value,
|
||||
'is_multiline' => $this->is_multiline,
|
||||
'is_literal' => $this->is_literal,
|
||||
'is_buildtime_only' => $this->is_buildtime_only,
|
||||
'is_runtime' => $this->is_runtime,
|
||||
'is_buildtime' => $this->is_buildtime,
|
||||
'is_preview' => $this->is_preview,
|
||||
]);
|
||||
$this->clear();
|
||||
@@ -68,6 +73,7 @@ class Add extends Component
|
||||
$this->value = '';
|
||||
$this->is_multiline = false;
|
||||
$this->is_literal = false;
|
||||
$this->is_buildtime_only = false;
|
||||
$this->is_runtime = true;
|
||||
$this->is_buildtime = true;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,8 @@ class All extends Component
|
||||
$environment->value = $data['value'];
|
||||
$environment->is_multiline = $data['is_multiline'] ?? false;
|
||||
$environment->is_literal = $data['is_literal'] ?? false;
|
||||
$environment->is_buildtime_only = $data['is_buildtime_only'] ?? false;
|
||||
$environment->is_runtime = $data['is_runtime'] ?? true;
|
||||
$environment->is_buildtime = $data['is_buildtime'] ?? true;
|
||||
$environment->is_preview = $data['is_preview'] ?? false;
|
||||
$environment->resourceable_id = $this->resource->id;
|
||||
$environment->resourceable_type = $this->resource->getMorphClass();
|
||||
|
||||
@@ -38,7 +38,9 @@ class Show extends Component
|
||||
|
||||
public bool $is_shown_once = false;
|
||||
|
||||
public bool $is_buildtime_only = false;
|
||||
public bool $is_runtime = true;
|
||||
|
||||
public bool $is_buildtime = true;
|
||||
|
||||
public bool $is_required = false;
|
||||
|
||||
@@ -58,7 +60,8 @@ class Show extends Component
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
'is_buildtime_only' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'real_value' => 'nullable',
|
||||
'is_required' => 'required|boolean',
|
||||
];
|
||||
@@ -102,7 +105,8 @@ class Show extends Component
|
||||
} else {
|
||||
$this->validate();
|
||||
$this->env->is_required = $this->is_required;
|
||||
$this->env->is_buildtime_only = $this->is_buildtime_only;
|
||||
$this->env->is_runtime = $this->is_runtime;
|
||||
$this->env->is_buildtime = $this->is_buildtime;
|
||||
$this->env->is_shared = $this->is_shared;
|
||||
}
|
||||
$this->env->key = $this->key;
|
||||
@@ -117,7 +121,8 @@ class Show extends Component
|
||||
$this->is_multiline = $this->env->is_multiline;
|
||||
$this->is_literal = $this->env->is_literal;
|
||||
$this->is_shown_once = $this->env->is_shown_once;
|
||||
$this->is_buildtime_only = $this->env->is_buildtime_only ?? false;
|
||||
$this->is_runtime = $this->env->is_runtime ?? true;
|
||||
$this->is_buildtime = $this->env->is_buildtime ?? true;
|
||||
$this->is_required = $this->env->is_required ?? false;
|
||||
$this->is_really_required = $this->env->is_really_required ?? false;
|
||||
$this->is_shared = $this->env->is_shared ?? false;
|
||||
|
||||
+65
-64
@@ -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');
|
||||
@@ -932,11 +933,11 @@ class Application extends BaseModel
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$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->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->custom_labels);
|
||||
$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->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->custom_labels.$this->settings->use_build_secrets);
|
||||
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal'])->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
|
||||
} else {
|
||||
$newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal'])->sort());
|
||||
$newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
|
||||
}
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -17,7 +17,8 @@ use OpenApi\Attributes as OA;
|
||||
'is_literal' => ['type' => 'boolean'],
|
||||
'is_multiline' => ['type' => 'boolean'],
|
||||
'is_preview' => ['type' => 'boolean'],
|
||||
'is_buildtime_only' => ['type' => 'boolean'],
|
||||
'is_runtime' => ['type' => 'boolean'],
|
||||
'is_buildtime' => ['type' => 'boolean'],
|
||||
'is_shared' => ['type' => 'boolean'],
|
||||
'is_shown_once' => ['type' => 'boolean'],
|
||||
'key' => ['type' => 'string'],
|
||||
@@ -37,13 +38,14 @@ class EnvironmentVariable extends BaseModel
|
||||
'value' => 'encrypted',
|
||||
'is_multiline' => 'boolean',
|
||||
'is_preview' => 'boolean',
|
||||
'is_buildtime_only' => 'boolean',
|
||||
'is_runtime' => 'boolean',
|
||||
'is_buildtime' => 'boolean',
|
||||
'version' => 'string',
|
||||
'resourceable_type' => 'string',
|
||||
'resourceable_id' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = ['real_value', 'is_shared', 'is_really_required'];
|
||||
protected $appends = ['real_value', 'is_shared', 'is_really_required', 'is_nixpacks', 'is_coolify'];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
@@ -137,6 +139,32 @@ class EnvironmentVariable extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
protected function isNixpacks(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (str($this->key)->startsWith('NIXPACKS_')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function isCoolify(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (str($this->key)->startsWith('SERVICE_')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected function isShared(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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