Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/Http/Controllers/Api/AuthenticatedApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public function getInstances(Request $request): JsonResponse
$formattedInstances = $instances->map(fn (PolydockAppInstance $instance) => [
'uuid' => $instance->uuid,
'name' => $instance->name,
'label' => $instance->getKeyValue('instance-label') ?: null,
'status' => $instance->status?->value,
'status_message' => $instance->status_message,
'app_url' => $instance->app_url,
Expand Down Expand Up @@ -147,6 +148,7 @@ public function getInstances(Request $request): JsonResponse
* @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 label string optional A free-form human-readable label for this instance. Not used as an identifier; may contain spaces and special characters. Example: "Acme Corp trial"
* @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"}}
* @bodyParam secret.ai object optional AI LLM configuration.
* @bodyParam secret.ai.llm_url string optional The LLM API base URL. Example: https://llm.local
Expand All @@ -164,6 +166,7 @@ public function getInstances(Request $request): JsonResponse
* "data": {
* "uuid": "3a105da1-9c87-43ca-9ac8-72787fc5e315",
* "name": "My awesome instance",
* "label": "Acme Corp trial",
* "status": "new"
* }
* }
Expand All @@ -176,6 +179,7 @@ public function createInstance(Request $request): JsonResponse
'last_name' => 'nullable|string|max:255',
'storeAppId' => 'required|string|exists:polydock_store_apps,uuid',
'name' => 'nullable|string|max:255',
'label' => 'nullable|string|max:255',
'secret' => 'nullable|array',
'secret.ai' => 'nullable|array',
'secret.ai.llm_url' => 'nullable|string',
Expand Down Expand Up @@ -266,6 +270,11 @@ function ($attribute, $value, $fail) {
]);
$instance->save();

// Store the optional free-form label
if ($request->filled('label')) {
$instance->storeKeyValue('instance-label', $request->input('label'));
}

// Handle top-level secret if provided
if ($request->filled('secret')) {
$instance->storeKeyValue('secret', $request->input('secret'));
Expand All @@ -287,6 +296,7 @@ function ($attribute, $value, $fail) {
'data' => [
'uuid' => $instance->uuid,
'name' => $instance->name,
'label' => $instance->getKeyValue('instance-label') ?: null,
'status' => $instance->status?->value,
],
], 201);
Expand Down
13 changes: 8 additions & 5 deletions app/Listeners/CreateWebhookCallForAppInstanceStatusChanged.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Listeners;

use App\Events\PolydockAppInstanceCreatedWithNewStatus;
use App\Events\PolydockAppInstanceStatusChanged;
use App\Models\PolydockStoreWebhookCall;
use Illuminate\Support\Facades\Log;
Expand All @@ -11,7 +12,7 @@ class CreateWebhookCallForAppInstanceStatusChanged
/**
* Handle the event.
*/
public function handle(PolydockAppInstanceStatusChanged $event): void
public function handle(PolydockAppInstanceStatusChanged|PolydockAppInstanceCreatedWithNewStatus $event): void
{
$webhooks = $event
->appInstance
Expand All @@ -30,31 +31,33 @@ public function handle(PolydockAppInstanceStatusChanged $event): void
return;
}

$previousStatus = property_exists($event, 'previousStatus') ? $event->previousStatus : null;

foreach ($webhooks as $webhook) {
PolydockStoreWebhookCall::create([
'polydock_store_webhook_id' => $webhook->id,
'event' => $event->previousStatus === null ? 'app_instance.created' : 'app_instance.status_changed',
'event' => $previousStatus === null ? 'app_instance.created' : 'app_instance.status_changed',
'payload' => [
'app_instance_id' => $event->appInstance->id,
'store_id' => $event->appInstance->storeApp->store->id,
'store_name' => $event->appInstance->storeApp->store->name,
'store_app_id' => $event->appInstance->polydock_store_app_id,
'store_app_name' => $event->appInstance->storeApp->name,
'previous_status' => $event->previousStatus?->value,
'previous_status' => $previousStatus?->value,
'current_status' => $event->appInstance->status->value,
'data' => $event->appInstance->getWebhookSafeData(),
'timestamp' => now()->toIso8601String(),
],
]);

Log::info(
$event->previousStatus === null
$previousStatus === null
? 'Created webhook call for new app instance'
: 'Created webhook call for app instance status change',
[
'webhook_id' => $webhook->id,
'app_instance_id' => $event->appInstance->id,
'previous_status' => $event->previousStatus?->value,
'previous_status' => $previousStatus?->value,
'current_status' => $event->appInstance->status->value,
],
);
Expand Down
1 change: 1 addition & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class EventServiceProvider extends ServiceProvider
],
PolydockAppInstanceCreatedWithNewStatus::class => [
ProcessNewPolydockAppInstance::class,
CreateWebhookCallForAppInstanceStatusChanged::class,
],
PolydockAppInstanceStatusChanged::class => [
CreateWebhookCallForAppInstanceStatusChanged::class,
Expand Down
77 changes: 77 additions & 0 deletions tests/Feature/Api/AuthenticatedApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -398,4 +398,81 @@ public function test_create_instance_with_nested_secrets(): void
$instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first();
$this->assertEquals($secret, $instance->getKeyValue('secret'));
}

public function test_create_instance_stores_and_returns_label(): void
{
Sanctum::actingAs($this->user, ['instances.write']);

$label = 'Acme Corp trial instance';

$response = $this->postJson('/api/instance', [
'email' => $this->user->email,
'storeAppId' => $this->storeApp->uuid,
'label' => $label,
]);

$response->assertCreated();
$this->assertEquals($label, $response->json('data.label'));

$instance = PolydockAppInstance::where('uuid', $response->json('data.uuid'))->first();
$this->assertEquals($label, $instance->getKeyValue('instance-label'));
}

public function test_create_instance_without_label_returns_null_label(): void
{
Sanctum::actingAs($this->user, ['instances.write']);

$response = $this->postJson('/api/instance', [
'email' => $this->user->email,
'storeAppId' => $this->storeApp->uuid,
]);

$response->assertCreated();
$this->assertNull($response->json('data.label'));
}

public function test_get_instances_returns_label(): void
{
Sanctum::actingAs($this->user, ['instances.read']);

$group = UserGroup::create(['name' => 'Label Test Group']);
$this->user->groups()->attach($group->id, ['role' => 'owner']);

$instance = PolydockAppInstance::create([
'polydock_store_app_id' => $this->storeApp->id,
'user_group_id' => $group->id,
'name' => 'labelled-instance',
'status' => PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED,
]);
$instance->storeKeyValue('instance-label', 'My readable label');

$response = $this->getJson('/api/instances?email='.$this->user->email);

$response->assertOk();
$found = collect($response->json('data'))->firstWhere('uuid', $instance->uuid);
$this->assertNotNull($found);
$this->assertEquals('My readable label', $found['label']);
}

public function test_get_instances_returns_null_label_when_not_set(): void
{
Sanctum::actingAs($this->user, ['instances.read']);

$group = UserGroup::create(['name' => 'No Label Group']);
$this->user->groups()->attach($group->id, ['role' => 'owner']);

PolydockAppInstance::create([
'polydock_store_app_id' => $this->storeApp->id,
'user_group_id' => $group->id,
'name' => 'unlabelled-instance',
'status' => PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED,
]);

$response = $this->getJson('/api/instances?email='.$this->user->email);

$response->assertOk();
$found = collect($response->json('data'))->firstWhere('name', 'unlabelled-instance');
$this->assertNotNull($found);
$this->assertNull($found['label']);
}
}
Loading