mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
200 lines
8.4 KiB
PHP
200 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\SharedEnvironmentVariable;
|
|
use App\Models\Team;
|
|
use App\Services\SharedVariableCanvas;
|
|
use App\Traits\EnvironmentVariableProtection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
|
|
/**
|
|
* Shared variables canvas: shows which resources use the shared variables of a scope, and adds
|
|
* a shared variable to a resource (a resource variable that holds {{scope.KEY}}) or removes it.
|
|
* Called by resources/js/environment-canvas.js through the /canvas/api session bridge.
|
|
*/
|
|
class SharedVariableCanvasController extends Controller
|
|
{
|
|
use EnvironmentVariableProtection;
|
|
|
|
public function __construct(private SharedVariableCanvas $canvas) {}
|
|
|
|
public function show(Request $request): JsonResponse
|
|
{
|
|
$scope = $this->scope($request);
|
|
$owner = $this->findOwner($scope, $request->route('uuid'));
|
|
if ($owner instanceof JsonResponse) {
|
|
return $owner;
|
|
}
|
|
$this->authorize('view', $owner);
|
|
|
|
return response()->json($this->canvas->payload($scope, $owner, $this->canEdit($request, $scope, $owner)));
|
|
}
|
|
|
|
public function storeLink(Request $request): JsonResponse
|
|
{
|
|
$scope = $this->scope($request);
|
|
$owner = $this->findOwner($scope, $request->route('uuid'));
|
|
if ($owner instanceof JsonResponse) {
|
|
return $owner;
|
|
}
|
|
$this->authorize('view', $owner);
|
|
|
|
$return = validateIncomingRequest($request);
|
|
if ($return instanceof JsonResponse) {
|
|
return $return;
|
|
}
|
|
$validator = Validator::make($request->all(), [
|
|
'source' => ['required', 'string', 'max:255'],
|
|
'target' => ['required', 'string', 'max:255'],
|
|
]);
|
|
if ($validator->fails()) {
|
|
return response()->json(['message' => 'Validation failed.', 'errors' => $validator->errors()], 422);
|
|
}
|
|
|
|
$variable = $this->findVariable($owner, $request->input('source'));
|
|
$resource = $this->canvas->resources($scope, $owner)->get($request->input('target'));
|
|
$errors = [];
|
|
if (! $variable) {
|
|
$errors['source'] = ['The source must be a shared variable of this scope.'];
|
|
}
|
|
if (! $resource) {
|
|
$errors['target'] = ['The target must be a resource that can use this shared variable.'];
|
|
}
|
|
if ($errors !== []) {
|
|
return response()->json(['message' => 'Validation failed.', 'errors' => $errors], 422);
|
|
}
|
|
$this->authorize('manageEnvironment', $resource);
|
|
|
|
$existing = $this->canvas->resourceVariables(collect([$resource]))->firstWhere('key', $variable->key);
|
|
if ($existing) {
|
|
$message = in_array($variable->key, $this->canvas->referencedKeys($scope, $existing->value), true)
|
|
? "{$resource->name} already uses {$variable->key}."
|
|
: "{$resource->name} already has a variable named {$variable->key}. Rename or remove it first.";
|
|
|
|
return response()->json(['message' => $message, 'errors' => ['target' => [$message]]], 422);
|
|
}
|
|
|
|
$environmentVariable = new EnvironmentVariable;
|
|
$environmentVariable->key = $variable->key;
|
|
$environmentVariable->value = $this->canvas->reference($scope, $variable->key);
|
|
$environmentVariable->is_multiline = (bool) $variable->is_multiline;
|
|
$environmentVariable->is_literal = (bool) $variable->is_literal;
|
|
$environmentVariable->resourceable_id = $resource->getKey();
|
|
$environmentVariable->resourceable_type = $resource->getMorphClass();
|
|
$environmentVariable->save();
|
|
|
|
$link = collect($this->canvas->links($scope, collect([$variable]), collect([$resource->uuid => $resource])))
|
|
->firstWhere('source', $this->canvas->variableNodeId($variable));
|
|
|
|
return response()->json($link, 201);
|
|
}
|
|
|
|
public function destroyLink(Request $request): JsonResponse
|
|
{
|
|
$scope = $this->scope($request);
|
|
$owner = $this->findOwner($scope, $request->route('uuid'));
|
|
if ($owner instanceof JsonResponse) {
|
|
return $owner;
|
|
}
|
|
$this->authorize('view', $owner);
|
|
|
|
[$source, $target] = array_pad(explode('|', (string) $request->route('link_id'), 2), 2, '');
|
|
$resource = $this->canvas->resources($scope, $owner)->get($target);
|
|
$variable = $this->findVariable($owner, $source);
|
|
$key = $variable?->key ?? (str_starts_with($source, 'missing:') ? substr($source, strlen('missing:')) : null);
|
|
if (! $resource || blank($key)) {
|
|
return response()->json(['message' => 'Link not found.'], 404);
|
|
}
|
|
$this->authorize('manageEnvironment', $resource);
|
|
|
|
// Preview copies of application variables are removed together with the variable.
|
|
$references = EnvironmentVariable::query()
|
|
->where('resourceable_type', $resource->getMorphClass())
|
|
->where('resourceable_id', $resource->getKey())
|
|
->get()
|
|
->filter(fn (EnvironmentVariable $environmentVariable) => in_array($key, $this->canvas->referencedKeys($scope, $environmentVariable->value), true));
|
|
if ($references->where('is_preview', false)->isEmpty()) {
|
|
return response()->json(['message' => 'Link not found.'], 404);
|
|
}
|
|
$embedded = $references->reject(fn (EnvironmentVariable $environmentVariable) => $this->canvas->isExactReference($scope, $key, $environmentVariable->value));
|
|
if ($embedded->isNotEmpty()) {
|
|
$message = "{$embedded->pluck('key')->unique()->join(', ')} on {$resource->name} uses {$this->canvas->reference($scope, $key)} inside a longer value. Edit it on the resource.";
|
|
|
|
return response()->json(['message' => $message], 422);
|
|
}
|
|
if ($this->usesDockerCompose($resource)) {
|
|
foreach ($references->pluck('key')->unique() as $environmentKey) {
|
|
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($environmentKey, $resource->docker_compose);
|
|
if ($isUsed) {
|
|
return response()->json(['message' => "Cannot remove {$environmentKey} from {$resource->name}. {$reason}"], 422);
|
|
}
|
|
}
|
|
}
|
|
|
|
DB::transaction(fn () => $references->each->delete());
|
|
|
|
return response()->json(['message' => 'Link removed.']);
|
|
}
|
|
|
|
/**
|
|
* The team routes have no {scope} segment.
|
|
*/
|
|
private function scope(Request $request): string
|
|
{
|
|
return $request->route('scope') ?? 'team';
|
|
}
|
|
|
|
private function findOwner(string $scope, ?string $uuid): Model|JsonResponse
|
|
{
|
|
$teamId = getTeamIdFromToken();
|
|
if (is_null($teamId)) {
|
|
return invalidTokenResponse();
|
|
}
|
|
$owner = match ($scope) {
|
|
'team' => Team::query()->find($teamId),
|
|
'project' => Project::query()->where('team_id', $teamId)->where('uuid', $uuid)->first(),
|
|
'environment' => Environment::query()->where('uuid', $uuid)->whereHas('project', fn ($query) => $query->where('team_id', $teamId))->with('project')->first(),
|
|
'server' => Server::query()->where('team_id', $teamId)->where('uuid', $uuid)->first(),
|
|
default => null,
|
|
};
|
|
|
|
return $owner ?? response()->json(['message' => str($scope)->headline().' not found.'], 404);
|
|
}
|
|
|
|
private function findVariable(Model $owner, ?string $nodeId): ?SharedEnvironmentVariable
|
|
{
|
|
if (! str_starts_with((string) $nodeId, 'variable:')) {
|
|
return null;
|
|
}
|
|
|
|
return $owner->environment_variables()->whereKey((int) substr($nodeId, strlen('variable:')))->first();
|
|
}
|
|
|
|
private function canEdit(Request $request, string $scope, Model $owner): bool
|
|
{
|
|
$user = $request->user();
|
|
$token = $user->currentAccessToken();
|
|
|
|
return $user->isAdminOfTeam($this->canvas->teamId($scope, $owner))
|
|
&& ($token->can('root') || $token->can('write'));
|
|
}
|
|
|
|
private function usesDockerCompose(Model $resource): bool
|
|
{
|
|
return $resource instanceof Service
|
|
|| ($resource instanceof Application && $resource->build_pack === 'dockercompose');
|
|
}
|
|
}
|