Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Http\Requests\Api\Application\ApplicationApiRequest;
use App\Models\Objects\DeploymentObject;
use App\Models\Server;
use App\Rules\DockerLabel;
use App\Services\Acl\Api\AdminAcl;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
Expand All @@ -17,6 +18,8 @@ class StoreServerRequest extends ApplicationApiRequest

/**
* Rules to be applied to this request.
*
* @return array<string, array<string|DockerLabel>|string>
*/
public function rules(): array
{
Expand All @@ -29,6 +32,8 @@ public function rules(): array
'user' => $rules['owner_id'],
'egg' => $rules['egg_id'],
'docker_image' => 'sometimes|string',
'docker_labels' => ['sometimes', 'array', new DockerLabel()],
'docker_labels.*' => 'required|string',
'startup' => 'sometimes|string',
'environment' => 'present|array',
'skip_scripts' => 'sometimes|boolean',
Expand Down Expand Up @@ -122,6 +127,7 @@ public function validated($key = null, $default = null): array
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
'backup_limit' => array_get($data, 'feature_limits.backups'),
'oom_killer' => array_get($data, 'oom_killer'),
'docker_labels' => array_get($data, 'docker_labels'),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,30 @@ public function rules(): array
'name' => $rules['name'],
'user' => $rules['owner_id'],
'description' => array_merge(['nullable'], $rules['description']),
'docker_labels' => 'sometimes|array',
'docker_labels.*' => 'string',
];
}

/**
* Convert the posted data into the correct format that is expected by the application.
*
* @return array<array-key, string>
* @return array<string, mixed>
*/
public function validated($key = null, $default = null): array
{
return [
$attributes = [
'external_id' => $this->input('external_id'),
'name' => $this->input('name'),
'owner_id' => $this->input('user'),
'description' => $this->input('description'),
];

if ($this->has('docker_labels')) {
$attributes['docker_labels'] = $this->input('docker_labels');
}

return $attributes;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions app/Models/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ class Server extends Model implements HasAvatar, Validatable
'skip_scripts' => ['sometimes', 'boolean'],
'image' => ['required', 'string', 'max:255'],
'icon' => ['sometimes', 'nullable', 'string'],
'docker_labels' => ['array'],
'docker_labels.*' => ['required', 'string'],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add use HasValidation { getRules as getValidationRules; }

    /**
     * @return array<string|string[]>
     */
    public static function getRules(): array
    {
        $rules = self::getValidationRules();

        $rules['docker_labels.*'][] = new DockerLabel();

        return $rules;
    }

'database_limit' => ['present', 'nullable', 'integer', 'min:0'],
'allocation_limit' => ['sometimes', 'nullable', 'integer', 'min:0'],
'backup_limit' => ['present', 'nullable', 'integer', 'min:0'],
Expand Down
27 changes: 27 additions & 0 deletions app/Rules/DockerLabel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class DockerLabel implements ValidationRule
{
/**
* Run the validation rule.
*
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if it's an array

Suggested change
{
{
if (!is_array($value)) {
$fail("{$attribute} must be an array.");
return;
}

foreach (array_keys($value) as $key) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add type check before calling array_keys().

The $value parameter is typed as mixed, so calling array_keys($value) directly will throw a TypeError if $value is not an array. Add a type check to handle non-array inputs gracefully.

Apply this diff:

     public function validate(string $attribute, mixed $value, Closure $fail): void
     {
+        if (!is_array($value)) {
+            $fail("{$attribute} must be an array.");
+            return;
+        }
+
         foreach (array_keys($value) as $key) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach (array_keys($value) as $key) {
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_array($value)) {
$fail("{$attribute} must be an array.");
return;
}
foreach (array_keys($value) as $key) {
🤖 Prompt for AI Agents
In app/Rules/DockerLabel.php around line 17, the code calls array_keys($value)
but $value is typed mixed; add an is_array($value) guard before calling
array_keys so non-array inputs are handled gracefully — if $value is not an
array return false (or otherwise short-circuit the validation) and only run
foreach(array_keys($value) ...) when is_array($value) is true.

// Docker labels are validated via https://regex101.com/r/FiYrwo/2 following Docker key format
// recommendations: https://docs.docker.com/engine/manage-resources/labels/
if (!preg_match('/^(?!(?:com\.docker\.|io\.docker\.|org\.dockerproject\.))(?=.*[a-z]$)[a-z](?:[a-z0-9]|(?<!\.)\.(?!\.)|(?<!-)-(?!-)|\/|_)*$/', $key)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this a constant with a description:

Keep those same comments with the constant.

@Boy132 we can piggyback off this later with real validation.

$fail("{$attribute} contains an invalid label: {$key}");

return;
}
}
}
}
13 changes: 10 additions & 3 deletions app/Services/Servers/DetailsModificationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public function __construct(private ConnectionInterface $connection, private Dae
* external_id: int,
* owner_id: int,
* name: string,
* description?: ?string
* description?: ?string,
* docker_labels?: array<string, string>
* } $data
*
* @throws Throwable
Expand All @@ -36,12 +37,18 @@ public function handle(Server $server, array $data): Server
return $this->connection->transaction(function () use ($data, $server) {
$owner = $server->owner_id;

$server->forceFill([
$attributes = [
'external_id' => Arr::get($data, 'external_id'),
'owner_id' => Arr::get($data, 'owner_id'),
'name' => Arr::get($data, 'name'),
'description' => Arr::get($data, 'description') ?? '',
])->saveOrFail();
];

if ($labels = Arr::get($data, 'docker_labels')) {
$attributes['docker_labels'] = $labels;
}

$server->forceFill($attributes)->saveOrFail();

// If the owner_id value is changed we need to revoke any tokens that exist for the server
// on the daemon instance so that the old owner no longer has any permission to access the
Expand Down
1 change: 1 addition & 0 deletions app/Transformers/Api/Application/ServerTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public function transform($server): array
'container' => [
'startup_command' => $server->startup,
'image' => $server->image,
'docker_labels' => $server->docker_labels ?? [],
// This field is deprecated, please use "status".
'installed' => $server->isInstalled() ? 1 : 0,
'environment' => $this->environmentService->handle($server),
Expand Down
51 changes: 51 additions & 0 deletions tests/Integration/Services/Servers/ServerCreationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,57 @@ public function test_server_without_allocation_is_created_with_deployment_object
$this->assertNull($response->allocation_id);
}

/**
* Test that docker labels are persisted when provided during server creation.
*/
public function test_server_creation_accepts_docker_labels(): void
{
/** @var User $user */
$user = User::factory()->create();

/** @var Node $node */
$node = Node::factory()->create();

/** @var Allocation $allocation */
$allocation = Allocation::factory()->create([
'node_id' => $node->id,
]);

$egg = $this->cloneEggAndVariables($this->bungeecord);

$labels = [
'com.example/app' => 'panel',
'tier' => 'production',
];

$data = [
'name' => $this->faker->name(),
'description' => $this->faker->sentence(),
'owner_id' => $user->id,
'allocation_id' => $allocation->id,
'node_id' => $allocation->node_id,
'memory' => 256,
'swap' => 128,
'disk' => 100,
'io' => 500,
'cpu' => 0,
'startup' => 'java -jar server.jar',
'image' => 'java:8',
'egg_id' => $egg->id,
'environment' => [
'BUNGEE_VERSION' => '123',
'SERVER_JARFILE' => 'server.jar',
],
'docker_labels' => $labels,
];

$this->daemonServerRepository->expects('setServer->create')->with(false)->andReturnUndefined();

$server = $this->getService()->handle($data);

$this->assertSame($labels, $server->docker_labels);
}

/**
* Test that a server is deleted from the Panel if daemon returns an error during the creation
* process.
Expand Down