feat(logging): strengthen secret redaction and add admin controls (#12151)

This commit is contained in:
Andras Bacsai
2026-10-13 20:43:19 +02:00
committed by GitHub
parent 9302395063
commit 448dc8ea0a
92 changed files with 4582 additions and 417 deletions
@@ -50,6 +50,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'is_env_sorting_enabled',
'redact_all_env_values_in_logs',
'is_pr_deployments_public_enabled',
'stop_grace_period',
'docker_images_to_keep',
@@ -76,6 +77,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile',
'include_source_commit_in_build',
'is_env_sorting_enabled',
'redact_all_env_values_in_logs',
'is_pr_deployments_public_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
@@ -429,6 +431,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -629,6 +632,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -829,6 +833,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -1000,6 +1005,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -1167,6 +1173,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -2700,7 +2707,10 @@ class ApplicationsController extends Controller
$lines = normalizeLogLines($request->query('lines'));
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps);
$logs = sanitizeLogsForExport(
getContainerLogs($application->destination->server, $container['ID'], $lines, $showTimestamps),
EnvironmentVariable::logRedactionSecretsFor($application, includePreview: $pullRequestId !== null),
);
return response()->json([
'logs' => $logs,
@@ -3172,6 +3182,7 @@ class ApplicationsController extends Controller
'inject_build_args_to_dockerfile' => ['type' => 'boolean', 'description' => 'Inject build arguments into the Dockerfile build.'],
'include_source_commit_in_build' => ['type' => 'boolean', 'description' => 'Include the source commit in the build.'],
'is_env_sorting_enabled' => ['type' => 'boolean', 'description' => 'Sort environment variables.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'is_pr_deployments_public_enabled' => ['type' => 'boolean', 'description' => 'Make pull request deployments public.'],
'stop_grace_period' => ['type' => 'integer', 'nullable' => true, 'minimum' => 1, 'maximum' => 3600, 'description' => 'Container stop grace period in seconds.'],
'docker_images_to_keep' => ['type' => 'integer', 'minimum' => 0, 'maximum' => 100, 'description' => 'Number of Docker images to retain.'],
@@ -3278,6 +3289,10 @@ class ApplicationsController extends Controller
$this->authorize('update', $application);
if ($request->has('redact_all_env_values_in_logs')) {
$this->authorize('manageEnvironment', $application);
}
if ($request->instant_deploy) {
abort_unless($request->user()->tokenCan('deploy') || $request->user()->tokenCan('root'), 403, 'Missing required permissions: deploy');
$this->authorize('deploy', $application);
@@ -18,6 +18,7 @@ use App\Models\LocalPersistentVolume;
use App\Models\Project;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\StandaloneDragonfly;
@@ -355,11 +356,19 @@ class DatabasesController extends Controller
}
$databaseIds = $databases->pluck('id')->toArray();
$databasesByMorphKey = $databases->keyBy(fn ($database) => $database->getMorphClass().':'.$database->id);
$messageSecrets = [];
$backupConfigs = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with(['latest_log', 's3', 's3Storages'])
->whereIn('database_id', $databaseIds)
->get()
->map(fn (ScheduledDatabaseBackup $backup) => $this->withBackupS3StorageUuids($backup))
->map(function (ScheduledDatabaseBackup $backup) use ($databasesByMorphKey, &$messageSecrets): ScheduledDatabaseBackup {
if ($backup->latest_log) {
$this->redactBackupExecutionMessage($backup->latest_log, $databasesByMorphKey->get($backup->database_type.':'.$backup->database_id), $messageSecrets);
}
return $this->withBackupS3StorageUuids($backup);
})
->groupBy(fn (ScheduledDatabaseBackup $backup) => $backup->database_type.':'.$backup->database_id);
$databases = $databases->map(function ($database) use ($backupConfigs) {
@@ -431,8 +440,15 @@ class DatabasesController extends Controller
$this->authorize('view', $database);
$messageSecrets = [];
$backupConfig = ScheduledDatabaseBackup::ownedByCurrentTeamAPI($teamId)->with(['executions', 's3', 's3Storages'])->where('database_id', $database->id)->get()
->map(fn (ScheduledDatabaseBackup $backup) => $this->withBackupS3StorageUuids($backup));
->map(function (ScheduledDatabaseBackup $backup) use ($database, &$messageSecrets): ScheduledDatabaseBackup {
$backup->executions->each(function (ScheduledDatabaseBackupExecution $execution) use ($database, &$messageSecrets): void {
$this->redactBackupExecutionMessage($execution, $database, $messageSecrets);
});
return $this->withBackupS3StorageUuids($backup);
});
return response()->json($backupConfig);
}
@@ -573,6 +589,7 @@ class DatabasesController extends Controller
'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.', 'minimum' => 1, 'default' => 5],
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.', 'minimum' => 1, 'default' => 5],
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.', 'minimum' => 0, 'default' => 5],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values, database passwords, and remote secrets are redacted. Requires team admin permissions.'],
],
),
)
@@ -627,6 +644,7 @@ class DatabasesController extends Controller
'limits_cpus' => 'string',
'limits_cpuset' => 'string|nullable',
'limits_cpu_shares' => 'numeric',
'redact_all_env_values_in_logs' => 'boolean',
]);
if ($validator->fails()) {
@@ -636,6 +654,7 @@ class DatabasesController extends Controller
], 422);
}
$uuid = $request->uuid;
$redactAllEnvValuesInLogs = $request->has('redact_all_env_values_in_logs') ? $request->boolean('redact_all_env_values_in_logs') : null;
removeUnnecessaryFieldsFromRequest($request);
$database = queryDatabaseByUuidWithinTeam($uuid, $teamId);
if (! $database) {
@@ -644,6 +663,10 @@ class DatabasesController extends Controller
$this->authorize('update', $database);
if ($redactAllEnvValuesInLogs !== null) {
$this->authorize('manageEnvironment', $database);
}
if ($request->is_public && $request->public_port) {
if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) {
return response()->json(['message' => 'Public port already used by another database.'], 400);
@@ -876,6 +899,9 @@ class DatabasesController extends Controller
}
// Only update database fields, not backup configuration
if ($redactAllEnvValuesInLogs !== null) {
$database->redact_all_env_values_in_logs = $redactAllEnvValuesInLogs;
}
$database->update($request->only($allowedFields));
if ($whatToDoWithDatabaseProxy === 'start') {
@@ -889,7 +915,10 @@ class DatabasesController extends Controller
'database_uuid' => $database->uuid,
'database_name' => $database->name,
'database_type' => $database->type(),
'changed_fields' => array_values(array_intersect($allowedFields, array_keys($request->all()))),
'changed_fields' => [
...array_values(array_intersect($allowedFields, array_keys($request->all()))),
...($redactAllEnvValuesInLogs === null ? [] : ['redact_all_env_values_in_logs']),
],
]);
return response()->json([
@@ -2783,7 +2812,10 @@ class DatabasesController extends Controller
$lines = normalizeLogLines($request->query('lines'));
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
$logs = getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps);
$logs = sanitizeLogsForExport(
getContainerLogs($database->destination->server, $container['ID'], $lines, $showTimestamps),
EnvironmentVariable::logRedactionSecretsFor($database),
);
return response()->json([
'logs' => $logs,
@@ -3145,7 +3177,7 @@ class DatabasesController extends Controller
#[OA\Get(
summary: 'List backup executions',
description: 'Get all executions for a specific backup configuration.',
description: 'Get all executions for a specific backup configuration. The execution `message` is only returned (redacted) to tokens with the `read:sensitive` or `root` ability; otherwise it is `null`.',
path: '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions',
operationId: 'list-backup-executions',
security: [
@@ -3185,7 +3217,7 @@ class DatabasesController extends Controller
new OA\Property(property: 'filename', type: 'string'),
new OA\Property(property: 'size', type: 'integer'),
new OA\Property(property: 'created_at', type: 'string'),
new OA\Property(property: 'message', type: 'string'),
new OA\Property(property: 'message', type: 'string', nullable: true, description: 'Redacted execution output. Null unless the token has the `read:sensitive` or `root` ability.'),
new OA\Property(property: 'status', type: 'string'),
]
)
@@ -3226,10 +3258,13 @@ class DatabasesController extends Controller
}
// Get all executions for this backup configuration
$messageSecrets = [];
$executions = $backup->executions()
->orderBy('created_at', 'desc')
->get()
->map(function ($execution) {
->map(function ($execution) use ($database, &$messageSecrets) {
$this->redactBackupExecutionMessage($execution, $database, $messageSecrets);
return [
'uuid' => $execution->uuid,
'filename' => $execution->filename,
@@ -5282,4 +5317,34 @@ class DatabasesController extends Controller
'message' => 'Database cloned.',
], 201);
}
/**
* Execution output can embed credentials: hide it unless the token can read sensitive data, and redact it when shown.
* The database secrets are loaded once per database and kept in $secretsByDatabase for the rest of the request.
*
* @param array<string, array{known: array<int, string>, locked: array<int, string>}|null> $secretsByDatabase
*/
private function redactBackupExecutionMessage(ScheduledDatabaseBackupExecution $execution, ?Model $database, array &$secretsByDatabase): void
{
$message = $execution->message;
if (request()->attributes->get('can_read_sensitive', false) !== true || ! is_string($message) || ! $database) {
$execution->setAttribute('message', null);
return;
}
$key = $database->getMorphClass().':'.$database->getKey();
if (! array_key_exists($key, $secretsByDatabase)) {
try {
// Use a copy so the relations loaded for redaction do not end up in the response.
$secretsByDatabase[$key] = EnvironmentVariable::logRedactionSecretsFor(clone $database);
} catch (\Throwable $e) {
report($e);
$secretsByDatabase[$key] = null;
}
}
// Without the database secrets the message cannot be redacted reliably, so hide it.
$execution->setAttribute('message', $secretsByDatabase[$key] === null ? null : sanitizeLogsForExport($message, $secretsByDatabase[$key]));
}
}
@@ -7,6 +7,7 @@ use App\Actions\Service\RestartServiceApplication;
use App\Actions\Service\StopServiceApplication;
use App\Actions\Service\UpdateServiceApplicationFromApi;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use App\Models\ServerSetting;
use App\Models\Service;
use App\Models\ServiceApplication;
@@ -532,7 +533,10 @@ class ServiceApplicationsController extends Controller
}
$lines = normalizeLogLines($request->query('lines'));
$logs = getContainerLogs($server, $containerName, $lines);
$logs = sanitizeLogsForExport(
getContainerLogs($server, $containerName, $lines),
EnvironmentVariable::logRedactionSecretsFor($serviceApplication->service),
);
return response()->json([
'logs' => $logs,
@@ -8,6 +8,7 @@ use App\Actions\Service\DeployServiceApplication;
use App\Actions\Service\RestartServiceApplication;
use App\Actions\Service\StopServiceApplication;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use App\Models\Service;
use App\Models\ServiceDatabase;
use Illuminate\Http\JsonResponse;
@@ -401,7 +402,10 @@ class ServiceDatabasesController extends Controller
$lines = normalizeLogLines($request->query('lines'));
return response()->json([
'logs' => getContainerLogs($server, $containerName, $lines),
'logs' => sanitizeLogsForExport(
getContainerLogs($server, $containerName, $lines),
EnvironmentVariable::logRedactionSecretsFor($serviceDatabase->service),
),
]);
}
@@ -321,6 +321,7 @@ class ServicesController extends Controller
],
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'default' => true, 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the service.'],
],
),
@@ -388,7 +389,7 @@ class ServicesController extends Controller
)]
public function create_service(Request $request)
{
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'tags'];
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs', 'tags'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -422,6 +423,7 @@ class ServicesController extends Controller
'urls.*.url' => ValidationPatterns::applicationDomainRules(),
'force_domain_override' => 'boolean',
'is_container_label_escape_enabled' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'tags' => 'array|nullable',
'tags.*' => 'string|min:2',
];
@@ -554,6 +556,9 @@ class ServicesController extends Controller
if ($request->has('is_container_label_escape_enabled')) {
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
}
if ($request->has('redact_all_env_values_in_logs')) {
$service->redact_all_env_values_in_logs = $request->boolean('redact_all_env_values_in_logs');
}
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
@@ -622,7 +627,7 @@ class ServicesController extends Controller
return response()->json(['message' => 'Service not found.', 'valid_service_types' => $serviceKeys], 404);
} elseif (filled($request->docker_compose_raw)) {
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'tags'];
$allowedFields = ['name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs', 'tags'];
$validationRules = [
'project_uuid' => 'string|required',
@@ -641,6 +646,7 @@ class ServicesController extends Controller
'urls.*.url' => ValidationPatterns::applicationDomainRules(),
'force_domain_override' => 'boolean',
'is_container_label_escape_enabled' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
'tags' => 'array|nullable',
'tags.*' => 'string|min:2',
];
@@ -757,6 +763,9 @@ class ServicesController extends Controller
if ($request->has('is_container_label_escape_enabled')) {
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
}
if ($request->has('redact_all_env_values_in_logs')) {
$service->redact_all_env_values_in_logs = $request->boolean('redact_all_env_values_in_logs');
}
$service->save();
$service->parse(isNew: true);
@@ -985,7 +994,10 @@ class ServicesController extends Controller
$lines = normalizeLogLines($request->query('lines'));
$showTimestamps = parseLogTimestampFlag($request->query('show_timestamps'));
$logs = getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps);
$logs = sanitizeLogsForExport(
getContainerLogs($service->destination->server, $container['ID'], $lines, $showTimestamps),
EnvironmentVariable::logRedactionSecretsFor($service),
);
return response()->json([
'logs' => $logs,
@@ -1132,6 +1144,7 @@ class ServicesController extends Controller
],
'force_domain_override' => ['type' => 'boolean', 'default' => false, 'description' => 'Force domain override even if conflicts are detected.'],
'is_container_label_escape_enabled' => ['type' => 'boolean', 'default' => true, 'description' => 'Escape special characters in labels. By default, $ (and other chars) is escaped. If you want to use env variables inside the labels, turn this off.'],
'redact_all_env_values_in_logs' => ['type' => 'boolean', 'default' => true, 'description' => 'Redact all environment variable values in logs. When false, only locked (shown once) values are redacted. Requires team admin permissions.'],
],
)
),
@@ -1220,12 +1233,16 @@ class ServicesController extends Controller
$this->authorize('update', $service);
if ($request->has('redact_all_env_values_in_logs')) {
$this->authorize('manageEnvironment', $service);
}
if ($request->boolean('instant_deploy')) {
abort_unless($request->user()->tokenCan('deploy') || $request->user()->tokenCan('root'), 403, 'Missing required permissions: deploy');
$this->authorize('deploy', $service);
}
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled'];
$allowedFields = ['name', 'description', 'instant_deploy', 'docker_compose_raw', 'connect_to_docker_network', 'urls', 'force_domain_override', 'is_container_label_escape_enabled', 'redact_all_env_values_in_logs'];
$validationRules = [
'name' => 'string|max:255',
@@ -1239,6 +1256,7 @@ class ServicesController extends Controller
'urls.*.url' => ValidationPatterns::applicationDomainRules(),
'force_domain_override' => 'boolean',
'is_container_label_escape_enabled' => 'boolean',
'redact_all_env_values_in_logs' => 'boolean',
];
$validationMessages = [
'urls.*.array' => 'An item in the urls array has invalid fields. Only name and url fields are supported.',
@@ -1306,6 +1324,9 @@ class ServicesController extends Controller
if ($request->has('is_container_label_escape_enabled')) {
$service->is_container_label_escape_enabled = $request->boolean('is_container_label_escape_enabled');
}
if ($request->has('redact_all_env_values_in_logs')) {
$service->redact_all_env_values_in_logs = $request->boolean('redact_all_env_values_in_logs');
}
$previousDnsHostnames = $this->serviceApplicationDnsHostnames($service);
$service->save();