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
61 changes: 30 additions & 31 deletions app/Http/Controllers/Api/AuthenticatedApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,20 +214,7 @@ public function getInstances(Request $request): JsonResponse
]);
}

if ($request->filled('group_id') && $request->filled('group_slug')) {
throw ValidationException::withMessages([
'group_id' => ['Only one of group_id or group_slug may be provided.'],
'group_slug' => ['Only one of group_id or group_slug may be provided.'],
]);
}

$targetGroup = null;

if (isset($validated['group_id'])) {
$targetGroup = UserGroup::findOrFail($validated['group_id']);
} elseif (isset($validated['group_slug'])) {
$targetGroup = UserGroup::where('slug', $validated['group_slug'])->firstOrFail();
}
$targetGroup = $this->resolveExistingGroupFromRequest($request, required: false);

/** @var User $actor */
$actor = $request->user();
Expand Down Expand Up @@ -497,25 +484,10 @@ public function assignInstanceToGroup(Request $request, string $uuid): JsonRespo
'group_slug' => 'nullable|string|exists:user_groups,slug',
]);

if (! $request->filled('group_id') && ! $request->filled('group_slug')) {
throw ValidationException::withMessages([
'group_id' => ['Either group_id or group_slug is required.'],
]);
}

if ($request->filled('group_id') && $request->filled('group_slug')) {
throw ValidationException::withMessages([
'group_id' => ['Only one of group_id or group_slug may be provided.'],
'group_slug' => ['Only one of group_id or group_slug may be provided.'],
]);
}
$group = $this->resolveExistingGroupFromRequest($request, required: true);

$instance = PolydockAppInstance::where('uuid', $uuid)->firstOrFail();

$group = isset($validated['group_id'])
? UserGroup::findOrFail($validated['group_id'])
: UserGroup::where('slug', $validated['group_slug'])->firstOrFail();

$this->authorize('assignToGroup', [$instance, $group]);

$oldGroupId = $instance->user_group_id;
Expand Down Expand Up @@ -631,8 +603,19 @@ public function deleteInstance(Request $request, string $uuid): JsonResponse
]);
}

private function resolveTargetGroup(Request $request, User $user): UserGroup
/**
* Resolve a group from the mutually-exclusive group_id / group_slug
* inputs — the shared read-path half of group resolution.
*/
private function resolveExistingGroupFromRequest(Request $request, bool $required): ?UserGroup
{
if ($request->filled('group_id') && $request->filled('group_slug')) {
throw ValidationException::withMessages([
'group_id' => ['Only one of group_id or group_slug may be provided.'],
'group_slug' => ['Only one of group_id or group_slug may be provided.'],
]);
}

if ($request->filled('group_id')) {
return UserGroup::findOrFail($request->integer('group_id'));
}
Expand All @@ -641,6 +624,22 @@ private function resolveTargetGroup(Request $request, User $user): UserGroup
return UserGroup::where('slug', $request->string('group_slug')->toString())->firstOrFail();
}

if ($required) {
throw ValidationException::withMessages([
'group_id' => ['Either group_id or group_slug is required.'],
]);
}

return null;
}

private function resolveTargetGroup(Request $request, User $user): UserGroup
{
$existing = $this->resolveExistingGroupFromRequest($request, required: false);
if ($existing) {
return $existing;
}

if ($request->filled('group_name')) {
$group = UserGroup::create([
'name' => $request->string('group_name')->toString(),
Expand Down
231 changes: 109 additions & 122 deletions app/Polydock/Apps/AmazeeClaw/Traits/Claim/ClaimAppInstanceTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,137 +13,124 @@ trait ClaimAppInstanceTrait
public function claimAppInstance(PolydockAppInstanceInterface $appInstance): PolydockAppInstanceInterface
{
$functionName = __FUNCTION__;
$logContext = $this->getLogContext($functionName);
$testLagoonPing = true;
$validateLagoonValues = true;
$validateLagoonProjectName = true;
$validateLagoonProjectId = true;

$this->info("{$functionName}: starting", $logContext);

$this->validateAppInstanceStatusIsExpectedAndConfigureLagoonClientAndVerifyLagoonValues(
return $this->runLifecyclePhase(
$appInstance,
$functionName,
PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM,
$logContext,
$testLagoonPing,
$validateLagoonValues,
$validateLagoonProjectName,
$validateLagoonProjectId
);

$projectName = $appInstance->getKeyValue('lagoon-project-name');
$deployEnvironment = $appInstance->getKeyValue('lagoon-deploy-branch');
$logContext += [
'projectName' => $projectName,
'deployEnvironment' => $deployEnvironment,
];

$this->info("{$functionName}: starting claim of project: {$projectName}", $logContext);
$appInstance->setStatus(
PolydockAppInstanceStatus::POLYDOCK_CLAIM_RUNNING,
PolydockAppInstanceStatus::POLYDOCK_CLAIM_RUNNING->getStatusMessage()
)->save();

$claimScript = $appInstance->getKeyValue('lagoon-claim-script');
$claimScriptService = $appInstance->getKeyValue('lagoon-claim-script-service') ?? 'openclaw-gateway';
$claimScriptContainer = $appInstance->getKeyValue('lagoon-claim-script-container') ?? 'node';

$logContext += [
'claimScript' => $claimScript,
'claimScriptService' => $claimScriptService,
'claimScriptContainer' => $claimScriptContainer,
];

try {
// Keep existing claim marker behavior.
$this->addOrUpdateLagoonProjectVariable($appInstance, 'POLYDOCK_CLAIMED_AT', date('Y-m-d H:i:s'), 'GLOBAL');

// Pre-warmed instances run post-create before a user is allocated,
// so user-email is empty there — set it (again) now that we have one.
$userEmail = (string) ($appInstance->getKeyValue('user-email') ?? '');
if ($userEmail !== '') {
$this->addOrUpdateLagoonProjectVariable($appInstance, 'POLYDOCK_USER_EMAIL', $userEmail, 'GLOBAL');
}

// User-mode keys are generated here, at claim time — the claiming
// user's email is not known during pre-warm/post-create.
if ($this->getRequiresAiInfrastructure()
&& $this->resolveAmazeeAiKeyMode($appInstance) === AmazeeAiKeyMode::User) {
if ($userEmail === '') {
// Without an email the backend would silently fall back to the
// anonymous @autogen.null identity — fail the claim instead of
// handing the user keys tied to the wrong identity.
throw new \Exception('AI key mode is "user" but no user-email is set on the instance — cannot generate per-user keys.');
}
$this->info("{$functionName}: Generating per-user AI keys via amazee.ai API", $logContext);
$this->generateAndStoreAmazeeAiCredentials($appInstance, $logContext, $userEmail);
}

// Provision/reuse credentials for the assigned user/team and inject into Lagoon.
$claimEnvironmentVariables = $this->provisionAndInjectManualAmazeeAiCredentials($appInstance, $logContext);

if (! empty($claimScript)) {
$this->info('Claim script', $logContext);
$claimScriptWithEnvironment = $this->buildClaimScriptWithInlineEnvironmentVariables($claimScript, $claimEnvironmentVariables);

$claimStdin = null;
if (! empty($claimEnvironmentVariables)) {
$claimStdin = '';
foreach ($claimEnvironmentVariables as $name => $value) {
$escapedValue = str_replace("'", "'\\''", $value);
$claimStdin .= "export {$name}='{$escapedValue}'\n";
PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED,
PolydockAppInstanceStatus::POLYDOCK_CLAIM_FAILED,
function (PolydockAppInstanceInterface $appInstance, array $logContext) use ($functionName): ?PolydockAppInstanceInterface {
$projectName = $appInstance->getKeyValue('lagoon-project-name');
$deployEnvironment = $appInstance->getKeyValue('lagoon-deploy-branch');
$logContext += [
'projectName' => $projectName,
'deployEnvironment' => $deployEnvironment,
];

$this->info("{$functionName}: starting claim of project: {$projectName}", $logContext);

$claimScript = $appInstance->getKeyValue('lagoon-claim-script');
$claimScriptService = $appInstance->getKeyValue('lagoon-claim-script-service') ?? 'openclaw-gateway';
$claimScriptContainer = $appInstance->getKeyValue('lagoon-claim-script-container') ?? 'node';

$logContext += [
'claimScript' => $claimScript,
'claimScriptService' => $claimScriptService,
'claimScriptContainer' => $claimScriptContainer,
];

try {
// Keep existing claim marker behavior.
$this->addOrUpdateLagoonProjectVariable($appInstance, 'POLYDOCK_CLAIMED_AT', date('Y-m-d H:i:s'), 'GLOBAL');

// Pre-warmed instances run post-create before a user is allocated,
// so user-email is empty there — set it (again) now that we have one.
$userEmail = (string) ($appInstance->getKeyValue('user-email') ?? '');
if ($userEmail !== '') {
$this->addOrUpdateLagoonProjectVariable($appInstance, 'POLYDOCK_USER_EMAIL', $userEmail, 'GLOBAL');
}
}

$claimResult = $this->lagoonClient->executeCommandOnProjectEnvironment(
$projectName,
$deployEnvironment,
$claimScriptWithEnvironment,
$claimScriptService,
$claimScriptContainer,
$claimStdin
);

$this->info('Claim result', $logContext + ['claimResult' => $claimResult]);

if (($claimResult['result'] ?? 1) !== 0) {
throw new \Exception(
($claimResult['result'] ?? '')
.' | '.($claimResult['result_text'] ?? '')
.' | '.($claimResult['error'] ?? '')
);
}
// User-mode keys are generated here, at claim time — the claiming
// user's email is not known during pre-warm/post-create.
if ($this->getRequiresAiInfrastructure()
&& $this->resolveAmazeeAiKeyMode($appInstance) === AmazeeAiKeyMode::User) {
if ($userEmail === '') {
// Without an email the backend would silently fall back to the
// anonymous @autogen.null identity — fail the claim instead of
// handing the user keys tied to the wrong identity.
throw new \Exception('AI key mode is "user" but no user-email is set on the instance — cannot generate per-user keys.');
}
$this->info("{$functionName}: Generating per-user AI keys via amazee.ai API", $logContext);
$this->generateAndStoreAmazeeAiCredentials($appInstance, $logContext, $userEmail);
}

if (! isset($claimResult['output'])) {
throw new \Exception(
'No output from claim command: '
.($claimResult['result'] ?? '')
.' | '.($claimResult['result_text'] ?? '')
.' | '.($claimResult['error'] ?? '')
);
}
// Provision/reuse credentials for the assigned user/team and inject into Lagoon.
$claimEnvironmentVariables = $this->provisionAndInjectManualAmazeeAiCredentials($appInstance, $logContext);

if (! empty($claimScript)) {
$this->info('Claim script', $logContext);
$claimScriptWithEnvironment = $this->buildClaimScriptWithInlineEnvironmentVariables($claimScript, $claimEnvironmentVariables);

$claimStdin = null;
if (! empty($claimEnvironmentVariables)) {
$claimStdin = '';
foreach ($claimEnvironmentVariables as $name => $value) {
$escapedValue = str_replace("'", "'\\''", $value);
$claimStdin .= "export {$name}='{$escapedValue}'\n";
}
}

$claimResult = $this->lagoonClient->executeCommandOnProjectEnvironment(
$projectName,
$deployEnvironment,
$claimScriptWithEnvironment,
$claimScriptService,
$claimScriptContainer,
$claimStdin
);

$this->info('Claim result', $logContext + ['claimResult' => $claimResult]);

if (($claimResult['result'] ?? 1) !== 0) {
throw new \Exception(
($claimResult['result'] ?? '')
.' | '.($claimResult['result_text'] ?? '')
.' | '.($claimResult['error'] ?? '')
);
}

if (! isset($claimResult['output'])) {
throw new \Exception(
'No output from claim command: '
.($claimResult['result'] ?? '')
.' | '.($claimResult['result_text'] ?? '')
.' | '.($claimResult['error'] ?? '')
);
}

if (! filter_var(trim((string) $claimResult['output']), FILTER_VALIDATE_URL)) {
throw new \Exception('Claim command output is not a valid URL: '.$claimResult['output']);
}

$appInstance->storeKeyValue('claim-command-output', trim((string) $claimResult['output']));
$appInstance->setAppUrl((string) $claimResult['output'], (string) $claimResult['output'], 24);
} else {
$this->info('No claim script detected', $logContext);
}
} catch (\Exception $e) {
$this->error($e->getMessage(), $logContext + [
'exception_class' => \get_class($e),
]);
$appInstance->setStatus(PolydockAppInstanceStatus::POLYDOCK_CLAIM_FAILED, substr($e->getMessage(), 0, 100))->save();

if (! filter_var(trim((string) $claimResult['output']), FILTER_VALIDATE_URL)) {
throw new \Exception('Claim command output is not a valid URL: '.$claimResult['output']);
return $appInstance;
}

$appInstance->storeKeyValue('claim-command-output', trim((string) $claimResult['output']));
$appInstance->setAppUrl((string) $claimResult['output'], (string) $claimResult['output'], 24);
} else {
$this->info('No claim script detected', $logContext);
}
} catch (\Exception $e) {
$this->error($e->getMessage(), $logContext + [
'exception_class' => \get_class($e),
]);
$appInstance->setStatus(PolydockAppInstanceStatus::POLYDOCK_CLAIM_FAILED, substr($e->getMessage(), 0, 100))->save();

return $appInstance;
}

$this->info("{$functionName}: completed", $logContext);
$appInstance->setStatus(PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED, 'Claim completed')->save();

return $appInstance;
return null;
},
'Claim completed',
);
}
}
Loading