Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions app/Http/Controllers/Api/ApplicationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson;
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
}
$repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost();
Expand Down Expand Up @@ -1461,7 +1461,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson;
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
}
$application->fqdn = $fqdn;
$application->git_repository = str($gitRepository)->trim()->toString();
Expand Down Expand Up @@ -1665,7 +1665,7 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson;
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
}
$application->fqdn = $fqdn;
$application->private_key_id = $privateKey->id;
Expand Down
22 changes: 22 additions & 0 deletions bootstrap/helpers/parsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,28 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
'is_preview' => false,
]);
}

// Also populate docker_compose_domains for dockercompose apps (mirrors Path 1 at lines 601-627)
if ($resource->build_pack === 'dockercompose') {
$normalizedFqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_')->value();
$serviceExists = false;
foreach ($services as $serviceNameKey => $svc) {
if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
$serviceExists = true;
break;
}
}
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
$domainExists = data_get($domains->get($normalizedFqdnFor), 'domain');
if (is_null($domainExists)) {
$domainValue = $fqdn;
$domains->put($normalizedFqdnFor, ['domain' => $domainValue]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
}
}
}
Comment on lines +507 to +528
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

I'll be back... to say this fix looks good, but $svc is unused.

The core logic correctly mirrors Path 1 (lines 636-649) to ensure KEY-based SERVICE_FQDN_* variables populate docker_compose_domains. This is the fix your resistance against serverless Docker Compose failures needed. However, static analysis correctly flagged an unused variable.

🔧 Proposed fix for unused variable
                 if ($resource->build_pack === 'dockercompose') {
                     $normalizedFqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_')->value();
                     $serviceExists = false;
-                    foreach ($services as $serviceNameKey => $svc) {
+                    foreach ($services as $serviceNameKey => $_) {
                         if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
                             $serviceExists = true;
                             break;
                         }
                     }

Or use array_keys() since you only need keys:

                 if ($resource->build_pack === 'dockercompose') {
                     $normalizedFqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_')->value();
                     $serviceExists = false;
-                    foreach ($services as $serviceNameKey => $svc) {
-                        if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
+                    foreach (array_keys($services) as $serviceNameKey) {
+                        if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
                             $serviceExists = true;
                             break;
                         }
                     }
📝 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
// Also populate docker_compose_domains for dockercompose apps (mirrors Path 1 at lines 601-627)
if ($resource->build_pack === 'dockercompose') {
$normalizedFqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_')->value();
$serviceExists = false;
foreach ($services as $serviceNameKey => $svc) {
if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
$serviceExists = true;
break;
}
}
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
$domainExists = data_get($domains->get($normalizedFqdnFor), 'domain');
if (is_null($domainExists)) {
$domainValue = $fqdn;
$domains->put($normalizedFqdnFor, ['domain' => $domainValue]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
}
}
}
// Also populate docker_compose_domains for dockercompose apps (mirrors Path 1 at lines 601-627)
if ($resource->build_pack === 'dockercompose') {
$normalizedFqdnFor = str($fqdnFor)->replace('-', '_')->replace('.', '_')->value();
$serviceExists = false;
foreach ($services as $serviceNameKey => $_) {
if (str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value() === $normalizedFqdnFor) {
$serviceExists = true;
break;
}
}
if ($serviceExists) {
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
$domainExists = data_get($domains->get($normalizedFqdnFor), 'domain');
if (is_null($domainExists)) {
$domainValue = $fqdn;
$domains->put($normalizedFqdnFor, ['domain' => $domainValue]);
$resource->docker_compose_domains = $domains->toJson();
$resource->save();
}
}
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 512-512: Avoid unused local variables such as '$svc'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bootstrap/helpers/parsers.php` around lines 507 - 528, The foreach loop
declares an unused $svc variable (foreach ($services as $serviceNameKey =>
$svc)) which static analysis flags; change the loop to iterate only keys (e.g.,
use array_keys($services) or foreach ($services as $serviceNameKey => $_) so
$svc is not declared) while keeping the existing logic that compares
str($serviceNameKey)->... to $normalizedFqdnFor and the subsequent
docker_compose_domains update (references: $services, $serviceNameKey, $svc,
$normalizedFqdnFor, docker_compose_domains).

}
}

Expand Down
144 changes: 144 additions & 0 deletions tests/Feature/ApplicationParserDockerComposeDomainsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

use App\Models\Application;
use App\Models\Environment;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\ServerSetting;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use phpseclib3\Crypt\EC;

uses(RefreshDatabase::class);

beforeEach(function () {
$this->user = User::factory()->create();
$this->team = Team::factory()->create();
$this->user->teams()->attach($this->team);

$this->project = Project::factory()->create(['team_id' => $this->team->id]);
$this->environment = Environment::factory()->create([
'project_id' => $this->project->id,
]);

$ecKey = EC::createKey('Ed25519');
$privateKeyContent = $ecKey->toString('OpenSSH');

$privateKey = PrivateKey::create([
'name' => 'test-key',
'private_key' => $privateKeyContent,
'team_id' => $this->team->id,
]);

$this->server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $privateKey->id,
]);

ServerSetting::create([
'server_id' => $this->server->id,
'wildcard_domain' => 'http://127.0.0.1.sslip.io',
]);

$this->destination = StandaloneDocker::factory()->create([
'server_id' => $this->server->id,
'network' => 'test-network-'.fake()->uuid(),
]);
});

test('applicationParser populates docker_compose_domains for KEY-based SERVICE_FQDN variables', function () {
$dockerCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
environment:
- SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_FQDN_FRONTEND=${FRONTEND_URL}
YAML;

$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);

applicationParser($application);

$application->refresh();

$domains = json_decode($application->docker_compose_domains, true);

expect($domains)->not->toBeNull()
->and($domains)->toBeArray()
->and($domains)->toHaveKey('backend')
->and($domains['backend'])->toHaveKey('domain')
->and($domains['backend']['domain'])->not->toBeEmpty();
});

test('applicationParser populates docker_compose_domains for KEY-based SERVICE_FQDN without port', function () {
$dockerCompose = <<<'YAML'
services:
frontend:
image: myapp/frontend:latest
environment:
- SERVICE_FQDN_FRONTEND=${FRONTEND_URL}
YAML;

$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockercompose',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);

applicationParser($application);

$application->refresh();

$domains = json_decode($application->docker_compose_domains, true);

expect($domains)->not->toBeNull()
->and($domains)->toHaveKey('frontend')
->and($domains['frontend'])->toHaveKey('domain');
});

test('applicationParser does not populate docker_compose_domains for non-dockercompose build_pack', function () {
$dockerCompose = <<<'YAML'
services:
backend:
image: myapp/backend:latest
environment:
- SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
YAML;

$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_id' => $this->destination->id,
'destination_type' => StandaloneDocker::class,
'build_pack' => 'dockerfile',
'docker_compose_raw' => $dockerCompose,
'fqdn' => null,
'docker_compose_domains' => null,
]);

applicationParser($application);

$application->refresh();

// For non-dockercompose, docker_compose_domains should remain empty
$domains = json_decode($application->docker_compose_domains, true);
expect($domains)->toBeNull();
});
Comment on lines +118 to +144
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Negative test case? I give it a positive review. The future is not serverless.

This test correctly verifies that non-dockercompose build packs don't get their domains populated. Essential guard against unintended side effects. The expect($domains)->toBeNull() assertion is the termination condition we need.

Consider adding an edge case test for when docker_compose_domains already has existing content to verify the merge behavior doesn't overwrite existing domains:

💡 Optional: Additional test for merge behavior
test('applicationParser preserves existing docker_compose_domains entries', function () {
    $dockerCompose = <<<'YAML'
services:
  backend:
    image: myapp/backend:latest
    environment:
      - SERVICE_FQDN_BACKEND_8000=${BACKEND_URL}
  frontend:
    image: myapp/frontend:latest
    environment:
      - SERVICE_FQDN_FRONTEND=${FRONTEND_URL}
YAML;

    $existingDomains = json_encode(['frontend' => ['domain' => 'https://existing.example.com']]);

    $application = Application::factory()->create([
        'environment_id' => $this->environment->id,
        'destination_id' => $this->destination->id,
        'destination_type' => StandaloneDocker::class,
        'build_pack' => 'dockercompose',
        'docker_compose_raw' => $dockerCompose,
        'fqdn' => null,
        'docker_compose_domains' => $existingDomains,
    ]);

    applicationParser($application);
    $application->refresh();

    $domains = json_decode($application->docker_compose_domains, true);

    // Existing frontend domain should be preserved
    expect($domains['frontend']['domain'])->toBe('https://existing.example.com')
        // New backend domain should be added
        ->and($domains)->toHaveKey('backend')
        ->and($domains['backend'])->toHaveKey('domain');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Feature/ApplicationParserDockerComposeDomainsTest.php` around lines 118
- 144, Add a new negative/edge-case test to ensure applicationParser preserves
existing docker_compose_domains when merging: create a test (e.g.,
test('applicationParser preserves existing docker_compose_domains entries',
...)) that uses Application::factory()->create with build_pack 'dockercompose',
a docker_compose_raw containing multiple services (e.g., backend and frontend),
and docker_compose_domains pre-populated (json_encode of an array with an
existing frontend domain); call applicationParser($application),
$application->refresh(), then assert that the pre-existing frontend domain
remains unchanged (inspect $application->docker_compose_domains via json_decode)
and that the new backend entry was added — reference applicationParser,
Application::factory, docker_compose_raw and docker_compose_domains when
implementing assertions.