From 5be19a9d22eb22538de301717085a2a5783e5f0b Mon Sep 17 00:00:00 2001 From: Philipp Melab Date: Tue, 24 Mar 2026 12:07:04 +0100 Subject: [PATCH 01/13] Add git_url to /store-apps API endpoint and tests --- app/Http/Controllers/Api/AuthenticatedApiController.php | 1 + tests/Feature/Api/AuthenticatedApiTest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 3e8f7fd..dace59e 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -49,6 +49,7 @@ public function getStoreApps(): JsonResponse 'author' => $app->author, 'available_for_trials' => $app->available_for_trials, 'app_status' => $app->status?->value, + 'git_url' => $app->lagoon_deploy_git, 'store' => [ 'name' => $app->store->name, 'status' => $app->store->status?->value, diff --git a/tests/Feature/Api/AuthenticatedApiTest.php b/tests/Feature/Api/AuthenticatedApiTest.php index df269fa..f2a3d1f 100644 --- a/tests/Feature/Api/AuthenticatedApiTest.php +++ b/tests/Feature/Api/AuthenticatedApiTest.php @@ -112,6 +112,7 @@ public function test_get_store_apps_returns_formatted_data(): void 'name', 'description', 'author', // etc... + 'git_url', 'store' => [ 'name', 'status', @@ -124,6 +125,7 @@ public function test_get_store_apps_returns_formatted_data(): void $this->assertCount(1, $response->json('data')); $this->assertEquals('Test App', $response->json('data.0.name')); $this->assertEquals('Test Store', $response->json('data.0.store.name')); + $this->assertEquals('git@github.com:example/repo.git', $response->json('data.0.git_url')); } public function test_get_instances_returns_user_instances(): void From 56dcfde16e4007f591a20d5637a5703885346070 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 14:13:33 +0100 Subject: [PATCH 02/13] chore: fix trigger deploy button --- app/Services/LagoonClientService.php | 63 ++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/app/Services/LagoonClientService.php b/app/Services/LagoonClientService.php index f294a75..0281367 100644 --- a/app/Services/LagoonClientService.php +++ b/app/Services/LagoonClientService.php @@ -17,12 +17,16 @@ public function getAuthenticatedClient(): Client $clientConfig = $this->getClientConfig(); if (! $clientConfig['ssh_private_key_file'] || ! file_exists($clientConfig['ssh_private_key_file'])) { - throw new \Exception('Global SSH private key not found.'); + $msg = 'Global SSH private key not found at: '.($clientConfig['ssh_private_key_file'] ?: 'not set'); + \Log::error($msg); + throw new \Exception($msg); } $token = $this->getLagoonToken($clientConfig); if (empty($token)) { - throw new \Exception('Failed to retrieve Lagoon API token.'); + $msg = 'Failed to retrieve Lagoon API token. Ensure the SSH key at '.$clientConfig['ssh_private_key_file'].' is valid and authorized in Lagoon.'; + \Log::error($msg); + throw new \Exception($msg); } return $this->buildClientWithToken($clientConfig, $token); @@ -54,12 +58,45 @@ public function getClientConfig(): array { $sshConfig = config('polydock.service_providers_singletons.PolydockServiceProviderFTLagoon', []); + // Primary source: config (which reads FTLAGOON_PRIVATE_KEY_FILE) + $keyFile = $sshConfig['ssh_private_key_file'] ?? null; + + // Fallback to POLYDOCK_LAGOON_DEPLOY_PRIVATE_KEY_FILE if first is missing or default + if (empty($keyFile) || $keyFile === 'tests/fixtures/lagoon-private-key') { + $keyFile = config('polydock.lagoon_deploy_private_key_file'); + } + + // Final fallback to system default + if (empty($keyFile)) { + $keyFile = getenv('HOME').'/.ssh/id_rsa'; + } + + // Fallback or override via content if provided + $keyContent = env('FTLAGOON_PRIVATE_KEY_CONTENT'); + + if ($keyContent) { + // Use storage/app/ssh as a safe default for writing the temp key + $baseDir = storage_path('app/ssh'); + + $tempKeyFile = $baseDir.'/env_id_rsa'; + + if (! is_dir(dirname($tempKeyFile))) { + mkdir(dirname($tempKeyFile), 0755, true); + } + + if (! file_exists($tempKeyFile) || file_get_contents($tempKeyFile) !== $keyContent) { + file_put_contents($tempKeyFile, $keyContent); + chmod($tempKeyFile, 0600); + } + $keyFile = $tempKeyFile; + } + return [ 'ssh_user' => $sshConfig['ssh_user'] ?? 'lagoon', 'ssh_server' => $sshConfig['ssh_server'] ?? 'ssh.lagoon.amazeeio.cloud', 'ssh_port' => $sshConfig['ssh_port'] ?? '32222', 'endpoint' => $sshConfig['endpoint'] ?? 'https://api.lagoon.amazeeio.cloud/graphql', - 'ssh_private_key_file' => $sshConfig['ssh_private_key_file'] ?? getenv('HOME').'/.ssh/id_rsa', + 'ssh_private_key_file' => $keyFile, ]; } @@ -81,6 +118,24 @@ public function getLagoonToken(?array $config = null): string privateKeyFile: $config['ssh_private_key_file'] ); - return $ssh->executeLagoonGetToken(); + // Add IdentitiesOnly to prevent fallback to local keys, making it fail faster and more predictably + $ssh->addExtraOption('-o IdentitiesOnly=yes'); + + $sshCommand = $ssh->getTokenCommand(); + $result = $ssh->executeRawSshCommand($sshCommand); + + if ($result['successful']) { + return ltrim(rtrim($result['output'])); + } + + \Log::error('Lagoon SSH token fetch failed', [ + 'exit_code' => $result['result'], + 'output' => $result['output'], + 'error' => $result['error'], + 'command' => $sshCommand, + 'key_file' => $config['ssh_private_key_file'], + ]); + + return ''; } } From d6f1eb76772bf3d77e700337bbe86a204c23a81f Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 15:23:45 +0100 Subject: [PATCH 03/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Services/LagoonClientService.php | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/app/Services/LagoonClientService.php b/app/Services/LagoonClientService.php index 0281367..2e1d1f3 100644 --- a/app/Services/LagoonClientService.php +++ b/app/Services/LagoonClientService.php @@ -68,11 +68,21 @@ public function getClientConfig(): array // Final fallback to system default if (empty($keyFile)) { - $keyFile = getenv('HOME').'/.ssh/id_rsa'; + $home = getenv('HOME'); + if ($home === false || $home === '') { + $home = $_SERVER['HOME'] ?? null; + } + + if (! empty($home)) { + $keyFile = rtrim($home, '/').'/.ssh/id_rsa'; + } else { + // Leave $keyFile empty; it will be validated later in getAuthenticatedClient() + $keyFile = null; + } } - // Fallback or override via content if provided - $keyContent = env('FTLAGOON_PRIVATE_KEY_CONTENT'); + // Fallback or override via content if provided (from config, not env()) + $keyContent = config('polydock.ftlagoon_private_key_content'); if ($keyContent) { // Use storage/app/ssh as a safe default for writing the temp key @@ -80,8 +90,11 @@ public function getClientConfig(): array $tempKeyFile = $baseDir.'/env_id_rsa'; - if (! is_dir(dirname($tempKeyFile))) { - mkdir(dirname($tempKeyFile), 0755, true); + $dir = dirname($tempKeyFile); + if (! is_dir($dir)) { + if (! @mkdir($dir, 0700, true) && ! is_dir($dir)) { + throw new \RuntimeException('Failed to create SSH key directory: '.$dir); + } } if (! file_exists($tempKeyFile) || file_get_contents($tempKeyFile) !== $keyContent) { From b1ff76198f6b169a9b7fc8f2d7bf191469e77663 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 18:03:38 +0100 Subject: [PATCH 04/13] chore: adapt ssh trigger deploy --- app/Services/LagoonClientService.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/Services/LagoonClientService.php b/app/Services/LagoonClientService.php index 2e1d1f3..16ec25c 100644 --- a/app/Services/LagoonClientService.php +++ b/app/Services/LagoonClientService.php @@ -4,6 +4,7 @@ use FreedomtechHosting\FtLagoonPhp\Client; use FreedomtechHosting\FtLagoonPhp\Ssh; +use Symfony\Component\Process\Process; class LagoonClientService { @@ -135,16 +136,18 @@ public function getLagoonToken(?array $config = null): string $ssh->addExtraOption('-o IdentitiesOnly=yes'); $sshCommand = $ssh->getTokenCommand(); - $result = $ssh->executeRawSshCommand($sshCommand); + $process = Process::fromShellCommandline($sshCommand); + $process->setTimeout(30); + $process->run(); - if ($result['successful']) { - return ltrim(rtrim($result['output'])); + if ($process->isSuccessful()) { + return ltrim(rtrim($process->getOutput())); } \Log::error('Lagoon SSH token fetch failed', [ - 'exit_code' => $result['result'], - 'output' => $result['output'], - 'error' => $result['error'], + 'exit_code' => $process->getExitCode(), + 'output' => $process->getOutput(), + 'error' => $process->getErrorOutput(), 'command' => $sshCommand, 'key_file' => $config['ssh_private_key_file'], ]); From 563b560a416bf797e44649687776eccba7c0f079 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 18:51:11 +0100 Subject: [PATCH 05/13] chore: support first and last name for api/instance --- .../Api/AuthenticatedApiController.php | 22 +++++++++++++++++-- .../ProgressToNextStageJob.php | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 6326923..8347e8b 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -170,6 +170,8 @@ public function createInstance(Request $request): JsonResponse { $request->validate([ 'email' => 'required|email', + 'first_name' => 'nullable|string|max:255', + 'last_name' => 'nullable|string|max:255', 'storeAppId' => 'required|string|exists:polydock_store_apps,uuid', 'name' => 'nullable|string|max:255', 'secret' => 'nullable|array', @@ -208,12 +210,23 @@ function ($attribute, $value, $fail) { $user = User::firstOrCreate( ['email' => $email], [ - 'first_name' => 'Auto', // Dummy default - 'last_name' => 'User', + 'first_name' => $request->input('first_name') ?? 'Auto', // Dummy default + 'last_name' => $request->input('last_name') ?? 'User', 'password' => Hash::make(Str::random(32)), ] ); + // Update user names if they were provided but user already existed + if ($request->filled('first_name')) { + $user->first_name = $request->input('first_name'); + } + if ($request->filled('last_name')) { + $user->last_name = $request->input('last_name'); + } + if ($user->isDirty()) { + $user->save(); + } + // Find or create a default primary user group for this user if they don't have one $primaryGroup = $user->primaryGroups()->first(); if (! $primaryGroup) { @@ -234,6 +247,11 @@ function ($attribute, $value, $fail) { // Use the existing allocation mechanism or create a new instance $instance = UserGroup::getNewAppInstanceForThisAppForThisGroup($storeApp, $primaryGroup, $name ? (string) $name : null); + // Add user information to the app instance data - this enables claiming + $instance->storeKeyValue('user-email', $user->email); + $instance->storeKeyValue('user-first-name', $user->first_name); + $instance->storeKeyValue('user-last-name', $user->last_name); + // Handle top-level secret if provided if ($request->filled('secret')) { $instance->storeKeyValue('secret', $request->input('secret')); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/ProgressToNextStageJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/ProgressToNextStageJob.php index 7944a83..f2036e5 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/ProgressToNextStageJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/ProgressToNextStageJob.php @@ -73,7 +73,7 @@ public function handle(): void ->save(); break; case PolydockAppInstanceStatus::POST_DEPLOY_COMPLETED: - if ($appInstance->remoteRegistration) { + if ($appInstance->remoteRegistration || $appInstance->getKeyValue('user-email')) { Log::info('Progressing app instance ' .$appInstance->id .' to next stage from POST_DEPLOY_COMPLETED to PENDING_POLYDOCK_CLAIM'); From d6375577f834b16702ae86374b7f683bff0028cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:17:41 +0000 Subject: [PATCH 06/13] Initial plan From 23dfde99d1df9860fbe58e97e458557f81e61462 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 19:17:45 +0100 Subject: [PATCH 07/13] Update app/Http/Controllers/Api/AuthenticatedApiController.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Api/AuthenticatedApiController.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 8347e8b..1dce3a2 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -216,12 +216,21 @@ function ($attribute, $value, $fail) { ] ); - // Update user names if they were provided but user already existed - if ($request->filled('first_name')) { - $user->first_name = $request->input('first_name'); - } - if ($request->filled('last_name')) { - $user->last_name = $request->input('last_name'); + // Update user names cautiously for existing users: + // Only replace placeholder/empty names, and do not arbitrarily overwrite real names. + if (! $user->wasRecentlyCreated) { + if ( + $request->filled('first_name') && + (\is_null($user->first_name) || $user->first_name === '' || $user->first_name === 'Auto') + ) { + $user->first_name = $request->input('first_name'); + } + if ( + $request->filled('last_name') && + (\is_null($user->last_name) || $user->last_name === '' || $user->last_name === 'User') + ) { + $user->last_name = $request->input('last_name'); + } } if ($user->isDirty()) { $user->save(); From 369044f9c59b0d99cf4067f79f15ce292f63832a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:18:10 +0000 Subject: [PATCH 08/13] Initial plan From 86852538badacb1c6d2d6ed72439ec8fd170a318 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:20:26 +0000 Subject: [PATCH 09/13] fix: use filled() for first_name/last_name defaults in firstOrCreate Co-authored-by: dan2k3k4 <158704+dan2k3k4@users.noreply.github.com> Agent-Logs-Url: https://github.com/amazeeio/polydock-engine/sessions/0b2ff62a-9be4-462b-b503-e3e8ab5cad64 --- app/Http/Controllers/Api/AuthenticatedApiController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 8347e8b..0dfe856 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -210,8 +210,8 @@ function ($attribute, $value, $fail) { $user = User::firstOrCreate( ['email' => $email], [ - 'first_name' => $request->input('first_name') ?? 'Auto', // Dummy default - 'last_name' => $request->input('last_name') ?? 'User', + 'first_name' => $request->filled('first_name') ? $request->input('first_name') : 'Auto', // Dummy default + 'last_name' => $request->filled('last_name') ? $request->input('last_name') : 'User', 'password' => Hash::make(Str::random(32)), ] ); From dccdc3374a527a39468cb6ac69dcfcbebb084d68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:20:40 +0000 Subject: [PATCH 10/13] chore: batch user info data updates into single DB write during instance provisioning Co-authored-by: dan2k3k4 <158704+dan2k3k4@users.noreply.github.com> Agent-Logs-Url: https://github.com/amazeeio/polydock-engine/sessions/8ac87baa-4aa8-4de5-b599-f0af18e2b880 --- app/Http/Controllers/Api/AuthenticatedApiController.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 1dce3a2..b11bd05 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -257,9 +257,12 @@ function ($attribute, $value, $fail) { $instance = UserGroup::getNewAppInstanceForThisAppForThisGroup($storeApp, $primaryGroup, $name ? (string) $name : null); // Add user information to the app instance data - this enables claiming - $instance->storeKeyValue('user-email', $user->email); - $instance->storeKeyValue('user-first-name', $user->first_name); - $instance->storeKeyValue('user-last-name', $user->last_name); + $instance->data = array_merge($instance->data ?? [], [ + 'user-email' => $user->email, + 'user-first-name' => $user->first_name, + 'user-last-name' => $user->last_name, + ]); + $instance->save(); // Handle top-level secret if provided if ($request->filled('secret')) { From 79b29a283916ed3855293396a10e0ec21b02b501 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 19:54:27 +0100 Subject: [PATCH 11/13] chore: fix docblock --- app/Http/Controllers/Api/AuthenticatedApiController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Api/AuthenticatedApiController.php b/app/Http/Controllers/Api/AuthenticatedApiController.php index 6912670..6381eb3 100644 --- a/app/Http/Controllers/Api/AuthenticatedApiController.php +++ b/app/Http/Controllers/Api/AuthenticatedApiController.php @@ -136,13 +136,15 @@ public function getInstances(Request $request): JsonResponse /** * Create/Provision a new instance * - * Deploy a new PolydockStoreApp instance. If the user associated with the email does not exist, a new user account will automatically be created. + * Deploy a new PolydockStoreApp instance. If the user associated with the email does not exist, a new user account will automatically be created using the provided first and last names. For existing users, names will be updated only if current values are placeholders or empty. * * @group External API * * @subgroup Instance Management * * @bodyParam email email required The email address of the user. Example: new.user@example.com + * @bodyParam first_name string optional The first name of the user. Example: Jane + * @bodyParam last_name string optional The last name of the user. Example: Doe * @bodyParam storeAppId string required The UUID of the store app to provision. Example: 3a105da1-9c87-43ca-9ac8-72787fc5e315 * @bodyParam name string optional The display name for this instance. Defaults to lagoon-project-name if not provided. Example: "My awesome instance" * @bodyParam secret object optional Sensitive AI and VectorDB credentials. Example: {"ai": {"llm_url": "https://llm", "api_key": "sk-123"}, "vector": {"db_host": "localhost", "db_port": 5432, "db_name": "db_d1234", "db_user": "admin", "db_pass": "pass"}} From 440ce983e2ecb254686d750492dd1e3849969bee Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 24 Mar 2026 21:05:58 +0100 Subject: [PATCH 12/13] chore: add tests --- tests/Feature/Api/AuthenticatedApiTest.php | 92 ++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/Feature/Api/AuthenticatedApiTest.php b/tests/Feature/Api/AuthenticatedApiTest.php index f2a3d1f..cae171c 100644 --- a/tests/Feature/Api/AuthenticatedApiTest.php +++ b/tests/Feature/Api/AuthenticatedApiTest.php @@ -174,6 +174,98 @@ public function test_create_instance_provisions_instance_and_creates_user(): voi $this->assertEquals(1, $instanceCount); } + public function test_create_instance_provisions_instance_and_creates_user_with_names(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $newEmail = 'named.user@example.com'; + $firstName = 'Jane'; + $lastName = 'Doe'; + + $response = $this->postJson('/api/instance', [ + 'email' => $newEmail, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'storeAppId' => $this->storeApp->uuid, + ]); + + $response->assertCreated(); + + $newUser = User::where('email', $newEmail)->first(); + $this->assertNotNull($newUser); + $this->assertEquals($firstName, $newUser->first_name); + $this->assertEquals($lastName, $newUser->last_name); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($newEmail, $instance->data['user-email']); + $this->assertEquals($firstName, $instance->data['user-first-name']); + $this->assertEquals($lastName, $instance->data['user-last-name']); + } + + public function test_create_instance_updates_placeholder_names_for_existing_user(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $existingUser = User::factory()->create([ + 'email' => 'placeholder@example.com', + 'first_name' => 'Auto', + 'last_name' => 'User', + ]); + + $firstName = 'Jane'; + $lastName = 'Doe'; + + $response = $this->postJson('/api/instance', [ + 'email' => $existingUser->email, + 'first_name' => $firstName, + 'last_name' => $lastName, + 'storeAppId' => $this->storeApp->uuid, + ]); + + $response->assertCreated(); + + $existingUser->refresh(); + $this->assertEquals($firstName, $existingUser->first_name); + $this->assertEquals($lastName, $existingUser->last_name); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($firstName, $instance->data['user-first-name']); + $this->assertEquals($lastName, $instance->data['user-last-name']); + } + + public function test_create_instance_does_not_overwrite_real_names_for_existing_user(): void + { + Sanctum::actingAs($this->user, ['instances.write']); + + $originalFirstName = 'John'; + $originalLastName = 'Smith'; + $existingUser = User::factory()->create([ + 'email' => 'real.name@example.com', + 'first_name' => $originalFirstName, + 'last_name' => $originalLastName, + ]); + + $newFirstName = 'Jane'; + $newLastName = 'Doe'; + + $response = $this->postJson('/api/instance', [ + 'email' => $existingUser->email, + 'first_name' => $newFirstName, + 'last_name' => $newLastName, + 'storeAppId' => $this->storeApp->uuid, + ]); + + $response->assertCreated(); + + $existingUser->refresh(); + $this->assertEquals($originalFirstName, $existingUser->first_name); + $this->assertEquals($originalLastName, $existingUser->last_name); + + $instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first(); + $this->assertEquals($originalFirstName, $instance->data['user-first-name']); + $this->assertEquals($originalLastName, $instance->data['user-last-name']); + } + public function test_get_instance_status(): void { Sanctum::actingAs($this->user, ['instances.read']); From 362247ebf0a558bfc391decf556d4eeb63bfea32 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Wed, 25 Mar 2026 17:03:15 +0100 Subject: [PATCH 13/13] Update app/Services/LagoonClientService.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/Services/LagoonClientService.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/Services/LagoonClientService.php b/app/Services/LagoonClientService.php index 16ec25c..386d762 100644 --- a/app/Services/LagoonClientService.php +++ b/app/Services/LagoonClientService.php @@ -99,8 +99,23 @@ public function getClientConfig(): array } if (! file_exists($tempKeyFile) || file_get_contents($tempKeyFile) !== $keyContent) { - file_put_contents($tempKeyFile, $keyContent); - chmod($tempKeyFile, 0600); + $tmpFile = $tempKeyFile.'.tmp'; + + $bytesWritten = @file_put_contents($tmpFile, $keyContent, LOCK_EX); + if ($bytesWritten === false) { + @unlink($tmpFile); + throw new \RuntimeException('Failed to write SSH private key to temporary file: '.$tmpFile); + } + + if (! @chmod($tmpFile, 0600)) { + @unlink($tmpFile); + throw new \RuntimeException('Failed to set permissions on SSH private key file: '.$tmpFile); + } + + if (! @rename($tmpFile, $tempKeyFile)) { + @unlink($tmpFile); + throw new \RuntimeException('Failed to move SSH private key file into place: '.$tempKeyFile); + } } $keyFile = $tempKeyFile; }