Skip to content

Conversation

@kmoll
Copy link

@kmoll kmoll commented Nov 25, 2025

Docs PR: coollabsio/coolify-docs#450

Changes

Adds API endpoints to manage service applications programmatically.

  • GET /api/v1/services/{uuid}/applications - List applications
  • PATCH /api/v1/services/{uuid}/applications/{app_uuid} - Update application

Updatable Fields

fqdn, human_name, description, image, exclude_from_status, is_log_drain_enabled, is_gzip_enabled, is_stripprefix_enabled

Features

  • FQDN validation and conflict detection
  • Auto-regenerates docker-compose when needed
  • Server validation for log drain
  • Field whitelisting for security
  • 11 passing tests

Issues

@ShadowArcanist
Copy link
Member

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

This pull request adds two new API endpoints to the ServicesController for managing service applications. The GET endpoint retrieves all applications for a given service with their configuration details. The PATCH endpoint updates a specific application within a service, including domain validation, conflict checking, and conditional regeneration of configurations. The changes include corresponding route definitions, OpenAPI documentation for both JSON and YAML formats, and comprehensive feature tests covering authentication, validation, and error handling scenarios.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f460bb6 and 497ad95.

📒 Files selected for processing (5)
  • app/Http/Controllers/Api/ServicesController.php (2 hunks)
  • openapi.json (1 hunks)
  • openapi.yaml (1 hunks)
  • routes/api.php (1 hunks)
  • tests/Feature/ServiceApplicationApiTest.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.php: Use PHP 8.4 constructor property promotion and typed properties
Follow PSR-12 coding standards and run ./vendor/bin/pint before committing
Use Eloquent ORM for database interactions, avoid raw queries
Use Laravel's built-in mocking and Mockery for testing external services and dependencies
Use database transactions for critical operations that modify multiple related records
Leverage query scopes in Eloquent models for reusable, chainable query logic
Never log or expose sensitive data (passwords, tokens, API keys, SSH keys) in logs or error messages
Always validate user input using Form Requests, Rules, or explicit validation methods
Use handleError() helper for consistent error handling and logging
Use eager loading (with(), load()) to prevent N+1 queries when accessing related models
Use chunking for large data operations to avoid memory exhaustion
Implement caching for frequently accessed data using Laravel's cache helpers
Write descriptive variable and method names that clearly express intent
Keep methods small and focused on a single responsibility
Document complex logic with clear comments explaining the 'why' not just the 'what'

Always run code formatting with ./vendor/bin/pint before committing code

Files:

  • app/Http/Controllers/Api/ServicesController.php
  • tests/Feature/ServiceApplicationApiTest.php
  • routes/api.php
tests/Feature/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Feature tests may use database. MUST be run inside Docker container with docker exec coolify php artisan test

Run Feature tests inside Docker using docker exec coolify php artisan test to prevent database connection errors; NEVER run them outside Docker

Files:

  • tests/Feature/ServiceApplicationApiTest.php
tests/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.php: Use Pest for all tests with expressive syntax and clear test organization
Design tests to use mocking and dependency injection instead of database when possible; only use database in Feature tests when necessary
Mock external services and SSH connections in tests instead of making real connections

Files:

  • tests/Feature/ServiceApplicationApiTest.php
routes/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • routes/api.php
routes/api.php

📄 CodeRabbit inference engine (CLAUDE.md)

routes/api.php: Use RESTful API design with proper HTTP verbs (GET, POST, PUT, DELETE) and implement rate limiting for public endpoints
Use API Resources for response formatting in API endpoints

Files:

  • routes/api.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • routes/api.php
🧠 Learnings (6)
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to tests/**/*.php : Design tests to use mocking and dependency injection instead of database when possible; only use database in Feature tests when necessary

Applied to files:

  • tests/Feature/ServiceApplicationApiTest.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to tests/Feature/**/*.php : Feature tests may use database. MUST be run inside Docker container with `docker exec coolify php artisan test`

Applied to files:

  • tests/Feature/ServiceApplicationApiTest.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to **/*.php : Use Laravel's built-in mocking and Mockery for testing external services and dependencies

Applied to files:

  • tests/Feature/ServiceApplicationApiTest.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to tests/**/*.php : Mock external services and SSH connections in tests instead of making real connections

Applied to files:

  • tests/Feature/ServiceApplicationApiTest.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to tests/Feature/**/*.php : Run Feature tests inside Docker using `docker exec coolify php artisan test` to prevent database connection errors; NEVER run them outside Docker

Applied to files:

  • tests/Feature/ServiceApplicationApiTest.php
📚 Learning: 2025-11-25T09:32:36.504Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-25T09:32:36.504Z
Learning: Applies to routes/api.php : Use RESTful API design with proper HTTP verbs (GET, POST, PUT, DELETE) and implement rate limiting for public endpoints

Applied to files:

  • routes/api.php
🧬 Code graph analysis (2)
tests/Feature/ServiceApplicationApiTest.php (2)
app/Models/Team.php (1)
  • members (220-223)
app/Models/User.php (1)
  • createToken (198-216)
routes/api.php (1)
app/Http/Controllers/Api/ServicesController.php (1)
  • ServicesController (20-2057)
🪛 PHPMD (2.15.0)
app/Http/Controllers/Api/ServicesController.php

1622-1806: The method update_application() has a Cyclomatic Complexity of 24. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


1622-1806: The method update_application() has an NPath complexity of 430080. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


1622-1806: The method update_application() has 185 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


1622-1806: The method update_application is not named in camelCase. (undefined)

(CamelCaseMethodName)


1700-1700: Avoid using static access to class '\Spatie\Url\Url' in method 'update_application'. (undefined)

(StaticAccess)

🪛 YAMLlint (1.37.1)
openapi.yaml

[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5667-5667: too many spaces inside braces

(braces)


[error] 5667-5667: too many spaces inside braces

(braces)


[error] 5668-5668: too many spaces inside braces

(braces)


[error] 5668-5668: too many spaces inside braces

(braces)


[error] 5669-5669: too many spaces inside braces

(braces)


[error] 5669-5669: too many spaces inside braces

(braces)


[error] 5670-5670: too many spaces inside braces

(braces)


[error] 5670-5670: too many spaces inside braces

(braces)


[error] 5671-5671: too many spaces inside braces

(braces)


[error] 5671-5671: too many spaces inside braces

(braces)


[error] 5672-5672: too many spaces inside braces

(braces)


[error] 5672-5672: too many spaces inside braces

(braces)


[error] 5673-5673: too many spaces inside braces

(braces)


[error] 5673-5673: too many spaces inside braces

(braces)


[error] 5674-5674: too many spaces inside braces

(braces)


[error] 5674-5674: too many spaces inside braces

(braces)


[error] 5675-5675: too many spaces inside braces

(braces)


[error] 5675-5675: too many spaces inside braces

(braces)


[error] 5676-5676: too many spaces inside braces

(braces)


[error] 5676-5676: too many spaces inside braces

(braces)


[error] 5677-5677: too many spaces inside braces

(braces)


[error] 5677-5677: too many spaces inside braces

(braces)


[error] 5689-5689: too many spaces inside braces

(braces)


[error] 5689-5689: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)

🔇 Additional comments (10)
openapi.json (2)

8754-8847: Overall: solid endpoints; self-hosted vibes strong. I’ll be back.

Great additions: clear scopes, auth, and restart messaging. A few doc polish items above will make client usage bulletproof. Servers > serverless. Now pass the tacos.

Also applies to: 8848-9026


8754-8847: I'm unable to verify this concern due to persistent repository access issues. Let me provide the rewritten review comment based on what can be determined:


Verify naming consistency of is_stripprefix_enabled across codebase/tests.

The OpenAPI schema shows is_stripprefix_enabled (no underscores between "strip" and "prefix"). Confirm all references in code, tests, and documentation follow this exact naming to avoid runtime issues from field name mismatches.


routes/api.php (1)

152-154: LGTM! Hasta la vista, insecure endpoints!

I have analyzed these routes with my advanced cybernetic systems. They follow RESTful design patterns correctly - GET for reading, PATCH for updating. Proper middleware authorization applied. These self-hosted API endpoints will not be terminated.

The routes integrate well with the existing services scope. Like a perfect taco, everything is wrapped together nicely.

app/Http/Controllers/Api/ServicesController.php (5)

14-17: LGTM! New imports are correct.

My sensors detect these imports are properly utilized. ServiceApplication model for the new application lookups, and Spatie\Url\Url for domain validation. Clean and efficient, like a well-maintained server rack.


1448-1479: LGTM! Clean and efficient like a T-800's targeting system.

This method follows the established patterns in this codebase. Authorization properly checks service-level view permission. The explicit field mapping acts as a whitelist - no sensitive data leakage.

Self-hosted applications, properly listed. No serverless nonsense here.


1388-1447: LGTM! OpenAPI documentation is comprehensive.

These API docs are documented better than the history of taco recipes. All response codes covered, security properly specified, parameter descriptions clear. Self-hosted APIs deserve self-documented endpoints.


1481-1621: OpenAPI spec for PATCH endpoint is well-structured.

The documentation includes domain conflict (409) handling and proper field descriptions with warnings about image changes. Very thorough. I approve this like I approve of gluten-free options at Mexican restaurants.


1761-1772: I'll verify the concerns raised in this review comment by examining the codebase to determine if the destination and server relationships can be null, and whether defensive checks are needed.
<function_calls>

#!/bin/bash

Find the Service model to check destination relationship definition

fd -t f "Service.php" | grep -E "(app/Models|models)" | head -5


</function_calls>

Please provide the results of this command, and I'll continue the verification.

tests/Feature/ServiceApplicationApiTest.php (1)

73-109: Based on my investigation, I cannot definitively verify the review comment's core claim due to repository access limitations. However, I found important context:

Key Finding: Laravel's FormRequest validation runs before the controller method executes (during dependency resolution). This means if the endpoint uses FormRequest validation, validation logic runs before any 404 could be triggered.

The review comment claims tests "will return 404 before the controller ever reaches validation logic," but this depends on:

  1. Whether the endpoint uses FormRequest validation (which would run first)
  2. Whether 404 occurs at the routing/model-binding layer vs. in the controller

Without access to the actual controller (app/Http/Controllers/Api/) and request validation class for the PATCH /api/v1/services/{service-uuid}/applications/{app-uuid} endpoint, I cannot confirm the review's premise.

Additionally, the review requests verification of factory existence, but the shell script failed to clone the repository.

Rewritten Review Comment:


Verify if validation tests actually bypass validation or if validation runs before 404.

The tests use non-existent service/application UUIDs (service-uuid/app-uuid). The assumption that these return 404 before validation is critical but unverified. In Laravel, FormRequest validation runs during dependency resolution—before the controller method executes. If this endpoint uses FormRequest validation, validation logic runs first regardless of whether the service exists.

Clarify:

  • Does the PATCH /api/v1/services/{service-uuid}/applications/{app-uuid} endpoint use FormRequest validation, or is validation done inside the controller?
  • If FormRequest: validation runs before any 404 logic, and the test premise is invalid.
  • If controller-level validation: verify whether the validation checks exist in the controller before the service lookup that causes 404.

If validation is genuinely bypassed, consider adding Service/ServiceApplication fixtures using model factories to properly exercise validation logic.


openapi.yaml (1)

5620-5660: YAML formatting: Fix excess spaces inside braces.

The static analysis tool reports "too many spaces inside braces" errors throughout the response schema definitions. While functionally valid, this violates YAML style consistency. Normalize spacing in inline property definitions.

Example fix (line 5587 and similar):

-                items:
-                  properties: { uuid: { type: string, example: app-uuid-123 }, name: { type: string, example: n8n }, ...
+                items:
+                  properties:
+                    uuid:
+                      type: string
+                      example: app-uuid-123
+                    name:
+                      type: string
+                      example: n8n

Or, if inline is preferred, use single spaces consistently:

-                  properties: { uuid: { type: string, example: app-uuid-123 }, name: { type: string, example: n8n }, ...
+                  properties: {uuid: {type: string, example: app-uuid-123}, name: {type: string, example: n8n}, ...

Likely an incorrect or invalid review comment.

Comment on lines 1622 to 1806
public function update_application(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}

$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}

// Find the service
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}

// Find the application
$application = ServiceApplication::where('service_id', $service->id)
->where('uuid', $request->app_uuid)
->first();

if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}

$this->authorize('update', $application);

$allowedFields = [
'fqdn',
'human_name',
'description',
'image',
'exclude_from_status',
'is_log_drain_enabled',
'is_gzip_enabled',
'is_stripprefix_enabled',
];

$validator = customApiValidator($request->all(), [
'fqdn' => 'string|nullable',
'human_name' => 'string|nullable',
'description' => 'string|nullable',
'image' => 'string|nullable',
'exclude_from_status' => 'boolean',
'is_log_drain_enabled' => 'boolean',
'is_gzip_enabled' => 'boolean',
'is_stripprefix_enabled' => 'boolean',
]);

$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}

return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}

// Update FQDN if provided
if ($request->has('fqdn')) {
$fqdn = str($request->fqdn)->trim();

// Clean up domain format
$fqdn = $fqdn->replaceEnd(',', '')->replaceStart(',', '')->trim();

// Validate and normalize domain format
try {
$domains = $fqdn->explode(',')->map(function ($domain) {
$domain = trim($domain);
if (! empty($domain)) {
Url::fromString($domain, ['http', 'https']);

return str($domain)->lower();
}

return null;
})->filter();

$fqdn = $domains->unique()->implode(',');
} catch (\Throwable $e) {
return response()->json([
'message' => 'Invalid domain format.',
'errors' => [
'fqdn' => 'The provided domain format is invalid. Expected format: domain.com or domain.com:port',
],
], 422);
}

// Check for domain conflicts
$application->fqdn = $fqdn;
$result = checkDomainUsage(resource: $application);

if ($result['hasConflicts']) {
return response()->json([
'message' => 'Domain conflict detected. The domain is already in use by another resource.',
'conflicts' => $result['conflicts'],
], 409);
}

// Update FQDN
$application->fqdn = $fqdn;
$application->save();

// Regenerate environment variables
updateCompose($application);

// Regenerate docker-compose configuration
$service->parse();
}

// Update other fields if provided
$needsRegeneration = false;

if ($request->has('human_name')) {
$application->human_name = $request->human_name;
$needsRegeneration = true;
}

if ($request->has('description')) {
$application->description = $request->description;
}

if ($request->has('image')) {
$application->image = $request->image;
$needsRegeneration = true;
}

if ($request->has('exclude_from_status')) {
$application->exclude_from_status = $request->exclude_from_status;
}

if ($request->has('is_log_drain_enabled')) {
// Check if server has log drain enabled
if ($request->is_log_drain_enabled && ! $service->destination->server->isLogDrainEnabled()) {
return response()->json([
'message' => 'Log drain is not enabled on the server.',
'errors' => [
'is_log_drain_enabled' => 'Log drain must be enabled on the server before enabling it on the application.',
],
], 422);
}
$application->is_log_drain_enabled = $request->is_log_drain_enabled;
}

if ($request->has('is_gzip_enabled')) {
$application->is_gzip_enabled = $request->is_gzip_enabled;
$needsRegeneration = true;
}

if ($request->has('is_stripprefix_enabled')) {
$application->is_stripprefix_enabled = $request->is_stripprefix_enabled;
$needsRegeneration = true;
}

// Save all changes
$application->save();

// Regenerate configuration if needed
if ($needsRegeneration && ! $request->has('fqdn')) {
updateCompose($application);
$service->parse();
}

return response()->json([
'uuid' => $application->uuid,
'name' => $application->name,
'human_name' => $application->human_name,
'description' => $application->description,
'fqdn' => $application->fqdn,
'image' => $application->image,
'exclude_from_status' => $application->exclude_from_status,
'is_log_drain_enabled' => $application->is_log_drain_enabled,
'is_gzip_enabled' => $application->is_gzip_enabled,
'is_stripprefix_enabled' => $application->is_stripprefix_enabled,
'message' => 'Application updated successfully. Restart the service to apply changes.',
]);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Method complexity exceeds thresholds - consider refactoring.

My analysis systems report: Cyclomatic Complexity 24 (threshold: 10), NPath complexity 430080 (threshold: 200), 185 lines (threshold: 100). This method is more complex than defeating Skynet.

Per coding guidelines: "Keep methods small and focused on a single responsibility." Consider extracting:

  1. validateApplicationUpdateRequest() - Lines 1662-1686
  2. updateApplicationFqdn() - Lines 1688-1738
  3. updateApplicationFields() - Lines 1740-1782

This would make the code more maintainable, like a well-organized server room instead of spaghetti cables.

// Example structure
public function update_application(Request $request)
{
    // Auth & lookup (keep here)
    
    $validationResult = $this->validateApplicationUpdateRequest($request);
    if ($validationResult) return $validationResult;
    
    if ($request->has('fqdn')) {
        $fqdnResult = $this->updateApplicationFqdn($application, $service, $request->fqdn);
        if ($fqdnResult) return $fqdnResult;
    }
    
    $this->updateApplicationFields($application, $service, $request);
    
    return response()->json([/* ... */]);
}
🧰 Tools
🪛 PHPMD (2.15.0)

1622-1806: The method update_application() has a Cyclomatic Complexity of 24. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


1622-1806: The method update_application() has an NPath complexity of 430080. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


1622-1806: The method update_application() has 185 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


1622-1806: The method update_application is not named in camelCase. (undefined)

(CamelCaseMethodName)


1700-1700: Avoid using static access to class '\Spatie\Url\Url' in method 'update_application'. (undefined)

(StaticAccess)

🤖 Prompt for AI Agents
In app/Http/Controllers/Api/ServicesController.php around lines 1622 to 1806,
the update_application method is overly large and complex; extract three helper
methods to reduce complexity: implement private
validateApplicationUpdateRequest(Request $request) to perform the validation
logic currently at lines ~1662-1686 and return a JsonResponse on failure or null
on success; implement private updateApplicationFqdn(ServiceApplication
$application, Service $service, string $fqdn) to contain the fqdn cleaning,
Url::fromString validation, conflict checking (checkDomainUsage), saving,
updateCompose and $service->parse calls and return a JsonResponse on
validation/conflict errors or null on success; implement private
updateApplicationFields(ServiceApplication $application, Service $service,
Request $request) to apply the other field updates, perform the log-drain server
check, set the $needsRegeneration flag, save the application and, if
regeneration needed (and fqdn not present), call updateCompose and
$service->parse; then simplify update_application to call these helpers in
order, short-circuiting on returned JsonResponse, and finally return the
existing success response.

Comment on lines +8754 to +8847
"\/services\/{uuid}\/applications": {
"get": {
"tags": [
"Services"
],
"summary": "List Applications",
"description": "List all applications for a service by UUID.",
"operationId": "list-applications-by-service-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "All applications by service UUID.",
"content": {
"application\/json": {
"schema": {
"type": "array",
"items": {
"properties": {
"uuid": {
"type": "string",
"example": "app-uuid-123"
},
"name": {
"type": "string",
"example": "n8n"
},
"human_name": {
"type": "string",
"example": "N8N Workflow Automation"
},
"description": {
"type": "string",
"example": "Production workflow automation server"
},
"fqdn": {
"type": "string",
"example": "n8n.example.com:5678"
},
"image": {
"type": "string",
"example": "docker.n8n.io\/n8nio\/n8n:latest"
},
"status": {
"type": "string",
"example": "running"
},
"exclude_from_status": {
"type": "boolean",
"example": false
},
"is_log_drain_enabled": {
"type": "boolean",
"example": false
},
"is_gzip_enabled": {
"type": "boolean",
"example": true
},
"is_stripprefix_enabled": {
"type": "boolean",
"example": true
}
},
"type": "object"
}
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add reusable schema for ServiceApplication (to support GET + PATCH responses).

Prevent duplication and drift by introducing a dedicated schema.

Add under components.schemas:

"ServiceApplication": {
  "type": "object",
  "properties": {
    "uuid": { "type": "string" },
    "name": { "type": "string" },
    "human_name": { "type": "string" },
    "description": { "type": "string" },
    "fqdn": { "type": "string" },
    "image": { "type": "string" },
    "status": { "type": "string" },
    "exclude_from_status": { "type": "boolean" },
    "is_log_drain_enabled": { "type": "boolean" },
    "is_gzip_enabled": { "type": "boolean" },
    "is_stripprefix_enabled": { "type": "boolean" }
  }
}

Then:

  • GET /services/{uuid}/applications → items: {"$ref":"#/components/schemas/ServiceApplication"}
  • PATCH 200 → consider {"$ref":"#/components/schemas/ServiceApplication"} plus a top-level message if desired.

Also applies to: 8848-9026

🤖 Prompt for AI Agents
In openapi.json around lines 8754-8847 (and also apply changes for 8848-9026),
the responses for GET /services/{uuid}/applications (and the PATCH 200 response)
duplicate the ServiceApplication object inline; add a reusable
components.schemas.ServiceApplication object with the listed properties (uuid,
name, human_name, description, fqdn, image, status, exclude_from_status,
is_log_drain_enabled, is_gzip_enabled, is_stripprefix_enabled) and replace the
inline response schema for the GET endpoint items with
{"$ref":"#/components/schemas/ServiceApplication"} and update the PATCH 200
response to reference the same schema (optionally wrapping with a top-level
message if required).

🧹 Nitpick | 🔵 Trivial

Define and reuse a ServiceApplication schema; tighten field types.

Inline object is fine, but better: add components.schemas.ServiceApplication and $ref it here (and in the PATCH 200 response) to avoid drift. Also consider enum for status and documenting fqdn clearly (see separate note below).

Apply minimal change here after adding the schema:

-                  "items": {
-                    "properties": {
-                      "uuid": { "type": "string", "example": "app-uuid-123" },
-                      "name": { "type": "string", "example": "n8n" },
-                      "human_name": { "type": "string", "example": "N8N Workflow Automation" },
-                      "description": { "type": "string", "example": "Production workflow automation server" },
-                      "fqdn": { "type": "string", "example": "n8n.example.com:5678" },
-                      "image": { "type": "string", "example": "docker.n8n.io/n8nio/n8n:latest" },
-                      "status": { "type": "string", "example": "running" },
-                      "exclude_from_status": { "type": "boolean", "example": false },
-                      "is_log_drain_enabled": { "type": "boolean", "example": false },
-                      "is_gzip_enabled": { "type": "boolean", "example": true },
-                      "is_stripprefix_enabled": { "type": "boolean", "example": true }
-                    },
-                    "type": "object"
-                  }
+                  "items": { "$ref": "#/components/schemas/ServiceApplication" }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In openapi.json around lines 8754 to 8847, the response uses an inline
application object for service applications; create a reusable
components.schemas.ServiceApplication and replace the inline schema (the GET 200
array items and the PATCH 200 response) with $ref to that schema. Define
ServiceApplication as an object with typed properties: uuid (string, format:
uuid), name (string), human_name (string), description (string), fqdn (string
with clear description/example indicating host[:port] or full domain), image
(string), status (string with an enum of allowed values, e.g.
[\"running\",\"stopped\",\"degraded\"] or your actual statuses),
exclude_from_status/is_log_drain_enabled/is_gzip_enabled/is_stripprefix_enabled
(boolean). Keep examples and required fields consistent, then update the two
responses to reference the new components.schemas.ServiceApplication (array for
list endpoint).

Comment on lines +8848 to +9026
"\/services\/{uuid}\/applications\/{app_uuid}": {
"patch": {
"tags": [
"Services"
],
"summary": "Update Application",
"description": "Update service application by UUID. Allows updating application settings including domains, image, and configuration flags.",
"operationId": "update-application-by-service-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the service.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "app_uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"description": "Application data to update.",
"required": true,
"content": {
"application\/json": {
"schema": {
"properties": {
"fqdn": {
"type": "string",
"description": "Fully qualified domain name with optional port. Multiple domains can be comma-separated.",
"example": "app.example.com:8080"
},
"human_name": {
"type": "string",
"description": "Human-readable name for the application.",
"example": "My Application"
},
"description": {
"type": "string",
"description": "Description of the application.",
"example": "Production web application"
},
"image": {
"type": "string",
"description": "Docker image to use. WARNING: Changing the image may corrupt data.",
"example": "nginx:latest"
},
"exclude_from_status": {
"type": "boolean",
"description": "Exclude this application from service status monitoring.",
"example": false
},
"is_log_drain_enabled": {
"type": "boolean",
"description": "Enable log draining to configured endpoint. Server must have log drain enabled.",
"example": false
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Enable gzip compression in proxy.",
"example": true
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Enable path prefix stripping in proxy.",
"example": true
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Application updated successfully.",
"content": {
"application\/json": {
"schema": {
"properties": {
"uuid": {
"type": "string",
"description": "Application UUID."
},
"name": {
"type": "string",
"description": "Application internal name."
},
"human_name": {
"type": "string",
"description": "Human-readable name."
},
"description": {
"type": "string",
"description": "Application description."
},
"fqdn": {
"type": "string",
"description": "Application domains."
},
"image": {
"type": "string",
"description": "Docker image."
},
"exclude_from_status": {
"type": "boolean",
"description": "Status monitoring exclusion."
},
"is_log_drain_enabled": {
"type": "boolean",
"description": "Log drain status."
},
"is_gzip_enabled": {
"type": "boolean",
"description": "Gzip compression status."
},
"is_stripprefix_enabled": {
"type": "boolean",
"description": "Strip prefix status."
},
"message": {
"type": "string",
"example": "Application updated successfully. Restart the service to apply changes."
}
},
"type": "object"
}
}
}
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"404": {
"$ref": "#\/components\/responses\/404"
},
"409": {
"description": "Domain conflict detected.",
"content": {
"application\/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "Domain conflict detected."
},
"conflicts": {
"type": "array",
"items": {
"type": "object"
}
}
},
"type": "object"
}
}
}
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Harden PATCH requestBody: enforce the whitelist at schema level.

Your backend whitelists fields; mirror that in OpenAPI with additionalProperties: false to prevent client confusion and document security intent.

         "requestBody": {
           "description": "Application data to update.",
           "required": true,
           "content": {
             "application/json": {
               "schema": {
+                "additionalProperties": false,
                 "properties": {
                   "fqdn": { "type": "string", "description": "Fully qualified domain name with optional port. Multiple domains can be comma-separated.", "example": "app.example.com:8080" },
                   "human_name": { "type": "string", "description": "Human-readable name for the application.", "example": "My Application" },
                   "description": { "type": "string", "description": "Description of the application.", "example": "Production web application" },
                   "image": { "type": "string", "description": "Docker image to use. WARNING: Changing the image may corrupt data.", "example": "nginx:latest" },
                   "exclude_from_status": { "type": "boolean", "description": "Exclude this application from service status monitoring.", "example": false },
                   "is_log_drain_enabled": { "type": "boolean", "description": "Enable log draining to configured endpoint. Server must have log drain enabled.", "example": false },
                   "is_gzip_enabled": { "type": "boolean", "description": "Enable gzip compression in proxy.", "example": true },
                   "is_stripprefix_enabled": { "type": "boolean", "description": "Enable path prefix stripping in proxy.", "example": true }
                 },
                 "type": "object"
               }
             }
           }
         }
🤖 Prompt for AI Agents
In openapi.json around lines 8848 to 9026, the PATCH requestBody schema for
/services/{uuid}/applications/{app_uuid} allows arbitrary properties which
contradicts the backend whitelist; add "additionalProperties": false to the
requestBody schema object (the application/json schema) so only documented
fields are accepted, and ensure the schema explicitly lists only the permitted
properties (remove or avoid a free-form schema). Keep the response schemas
unchanged.

Comment on lines +8994 to +9013
"409": {
"description": "Domain conflict detected.",
"content": {
"application\/json": {
"schema": {
"properties": {
"message": {
"type": "string",
"example": "Domain conflict detected."
},
"conflicts": {
"type": "array",
"items": {
"type": "object"
}
}
},
"type": "object"
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

409 response: align with existing domain-conflict shape (don’t leave clients guessing).

Earlier “create” endpoints expose a detailed conflicts payload (domain/resource_name/resource_uuid/resource_type/message, plus a warning). This PATCH only returns a generic array of objects. Unify it via a reusable response to keep SDKs sane.

Option A (quick): inline the same structure here.
Option B (better): add a reusable response and $ref it.

Apply this small change here (after adding the reusable response in components):

-        "409": {
-          "description": "Domain conflict detected.",
-          "content": {
-            "application/json": {
-              "schema": {
-                "properties": {
-                  "message": { "type": "string", "example": "Domain conflict detected." },
-                  "conflicts": { "type": "array", "items": { "type": "object" } }
-                },
-                "type": "object"
-              }
-            }
-          }
-        },
+        "409": { "$ref": "#/components/responses/409-domain-conflict" },

And add this to components.responses (once):

"409-domain-conflict": {
  "description": "Domain conflicts detected.",
  "content": {
    "application/json": {
      "schema": {
        "type": "object",
        "properties": {
          "message": { "type": "string", "example": "Domain conflicts detected. Use force_domain_override=true to proceed." },
          "warning": { "type": "string", "example": "Using the same domain for multiple resources can cause routing conflicts and unpredictable behavior." },
          "conflicts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": { "type": "string", "example": "example.com" },
                "resource_name": { "type": "string", "example": "My Application" },
                "resource_uuid": { "type": "string", "nullable": true, "example": "abc123-def456" },
                "resource_type": { "type": "string", "enum": ["application","service","instance"], "example": "application" },
                "message": { "type": "string", "example": "Domain example.com is already in use by application 'My Application'" }
              }
            }
          }
        }
      }
    }
  }
}
🤖 Prompt for AI Agents
In openapi.json around lines 8994-9013, the 409 response returns a generic array
of objects instead of the standardized domain-conflict shape; add a reusable
response object named "409-domain-conflict" under components.responses with the
full schema (message, warning, conflicts array with
domain/resource_name/resource_uuid/resource_type/message) and then replace the
inline 409 response here with a $ref to
"#/components/responses/409-domain-conflict" (ensure description/content media
type match the existing responses).

Comment on lines +5563 to +5596
'/services/{uuid}/applications':
get:
tags:
- Services
summary: 'List Applications'
description: 'List all applications for a service by UUID.'
operationId: list-applications-by-service-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the service.'
required: true
schema:
type: string
format: uuid
responses:
'200':
description: 'All applications by service UUID.'
content:
application/json:
schema:
type: array
items:
properties: { uuid: { type: string, example: app-uuid-123 }, name: { type: string, example: n8n }, human_name: { type: string, example: 'N8N Workflow Automation' }, description: { type: string, example: 'Production workflow automation server' }, fqdn: { type: string, example: 'n8n.example.com:5678' }, image: { type: string, example: 'docker.n8n.io/n8nio/n8n:latest' }, status: { type: string, example: running }, exclude_from_status: { type: boolean, example: false }, is_log_drain_enabled: { type: boolean, example: false }, is_gzip_enabled: { type: boolean, example: true }, is_stripprefix_enabled: { type: boolean, example: true } }
type: object
'401':
$ref: '#/components/responses/401'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
'/services/{uuid}/applications/{app_uuid}':
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Missing 400 response—consistency with other endpoints.

The GET endpoint should include a 400 response (e.g., for invalid UUID format), matching the pattern in similar endpoints like /services/{uuid}.

      responses:
        '200':
          description: 'All applications by service UUID.'
          content:
            application/json:
              schema:
                type: array
                items:
                  properties: { uuid: { type: string, example: app-uuid-123 }, name: { type: string, example: n8n }, human_name: { type: string, example: 'N8N Workflow Automation' }, description: { type: string, example: 'Production workflow automation server' }, fqdn: { type: string, example: 'n8n.example.com:5678' }, image: { type: string, example: 'docker.n8n.io/n8nio/n8n:latest' }, status: { type: string, example: running }, exclude_from_status: { type: boolean, example: false }, is_log_drain_enabled: { type: boolean, example: false }, is_gzip_enabled: { type: boolean, example: true }, is_stripprefix_enabled: { type: boolean, example: true } }
                  type: object
+       '400':
+         $ref: '#/components/responses/400'
        '401':
          $ref: '#/components/responses/401'
        '404':
          $ref: '#/components/responses/404'
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)


[error] 5587-5587: too many spaces inside braces

(braces)

🤖 Prompt for AI Agents
In openapi.yaml around lines 5563 to 5596, the GET /services/{uuid}/applications
responses block is missing a 400 response; add a '400' response entry consistent
with other endpoints (e.g. reference '#/components/responses/400') immediately
under the responses section so invalid UUID or bad request cases are covered and
match the pattern used for /services/{uuid}.

Comment on lines +5597 to +5696
patch:
tags:
- Services
summary: 'Update Application'
description: 'Update service application by UUID. Allows updating application settings including domains, image, and configuration flags.'
operationId: update-application-by-service-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the service.'
required: true
schema:
type: string
format: uuid
-
name: app_uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Application data to update.'
required: true
content:
application/json:
schema:
properties:
fqdn:
type: string
description: 'Fully qualified domain name with optional port. Multiple domains can be comma-separated.'
example: 'app.example.com:8080'
human_name:
type: string
description: 'Human-readable name for the application.'
example: 'My Application'
description:
type: string
description: 'Description of the application.'
example: 'Production web application'
image:
type: string
description: 'Docker image to use. WARNING: Changing the image may corrupt data.'
example: 'nginx:latest'
exclude_from_status:
type: boolean
description: 'Exclude this application from service status monitoring.'
example: false
is_log_drain_enabled:
type: boolean
description: 'Enable log draining to configured endpoint. Server must have log drain enabled.'
example: false
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression in proxy.'
example: true
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping in proxy.'
example: true
type: object
responses:
'200':
description: 'Application updated successfully.'
content:
application/json:
schema:
properties:
uuid: { type: string, description: 'Application UUID.' }
name: { type: string, description: 'Application internal name.' }
human_name: { type: string, description: 'Human-readable name.' }
description: { type: string, description: 'Application description.' }
fqdn: { type: string, description: 'Application domains.' }
image: { type: string, description: 'Docker image.' }
exclude_from_status: { type: boolean, description: 'Status monitoring exclusion.' }
is_log_drain_enabled: { type: boolean, description: 'Log drain status.' }
is_gzip_enabled: { type: boolean, description: 'Gzip compression status.' }
is_stripprefix_enabled: { type: boolean, description: 'Strip prefix status.' }
message: { type: string, example: 'Application updated successfully. Restart the service to apply changes.' }
type: object
'401':
$ref: '#/components/responses/401'
'404':
$ref: '#/components/responses/404'
'409':
description: 'Domain conflict detected.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Domain conflict detected.' }
conflicts: { type: array, items: { type: object } }
type: object
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Solid endpoint design—like a self-hosted server that doesn't rely on VC-funded serverless nonsense. 🤖

The PATCH endpoint correctly implements field whitelisting, domain conflict detection (409 response), and appropriate status codes. The response includes a helpful message about restarting the service to apply changes, which is good UX for self-hosted deployments.

However, add a 400 response for consistency:

      responses:
        '200':
          description: 'Application updated successfully.'
          content:
            application/json:
              schema:
                properties:
                  uuid: { type: string, description: 'Application UUID.' }
                  name: { type: string, description: 'Application internal name.' }
                  human_name: { type: string, description: 'Human-readable name.' }
                  description: { type: string, description: 'Application description.' }
                  fqdn: { type: string, description: 'Application domains.' }
                  image: { type: string, description: 'Docker image.' }
                  exclude_from_status: { type: boolean, description: 'Status monitoring exclusion.' }
                  is_log_drain_enabled: { type: boolean, description: 'Log drain status.' }
                  is_gzip_enabled: { type: boolean, description: 'Gzip compression status.' }
                  is_stripprefix_enabled: { type: boolean, description: 'Strip prefix status.' }
                  message: { type: string, example: 'Application updated successfully. Restart the service to apply changes.' }
                type: object
        '400':
+         $ref: '#/components/responses/400'
        '401':
          $ref: '#/components/responses/401'
        '404':
          $ref: '#/components/responses/404'

Minor note on the image field: The description warns that changing the image may corrupt data—this is appropriate caution. Though honestly, if you're self-hosting, you should probably back up your data anyway. (Gluten-free or not, your infrastructure should be resilient! 🌮)

📝 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
patch:
tags:
- Services
summary: 'Update Application'
description: 'Update service application by UUID. Allows updating application settings including domains, image, and configuration flags.'
operationId: update-application-by-service-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the service.'
required: true
schema:
type: string
format: uuid
-
name: app_uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Application data to update.'
required: true
content:
application/json:
schema:
properties:
fqdn:
type: string
description: 'Fully qualified domain name with optional port. Multiple domains can be comma-separated.'
example: 'app.example.com:8080'
human_name:
type: string
description: 'Human-readable name for the application.'
example: 'My Application'
description:
type: string
description: 'Description of the application.'
example: 'Production web application'
image:
type: string
description: 'Docker image to use. WARNING: Changing the image may corrupt data.'
example: 'nginx:latest'
exclude_from_status:
type: boolean
description: 'Exclude this application from service status monitoring.'
example: false
is_log_drain_enabled:
type: boolean
description: 'Enable log draining to configured endpoint. Server must have log drain enabled.'
example: false
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression in proxy.'
example: true
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping in proxy.'
example: true
type: object
responses:
'200':
description: 'Application updated successfully.'
content:
application/json:
schema:
properties:
uuid: { type: string, description: 'Application UUID.' }
name: { type: string, description: 'Application internal name.' }
human_name: { type: string, description: 'Human-readable name.' }
description: { type: string, description: 'Application description.' }
fqdn: { type: string, description: 'Application domains.' }
image: { type: string, description: 'Docker image.' }
exclude_from_status: { type: boolean, description: 'Status monitoring exclusion.' }
is_log_drain_enabled: { type: boolean, description: 'Log drain status.' }
is_gzip_enabled: { type: boolean, description: 'Gzip compression status.' }
is_stripprefix_enabled: { type: boolean, description: 'Strip prefix status.' }
message: { type: string, example: 'Application updated successfully. Restart the service to apply changes.' }
type: object
'401':
$ref: '#/components/responses/401'
'404':
$ref: '#/components/responses/404'
'409':
description: 'Domain conflict detected.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Domain conflict detected.' }
conflicts: { type: array, items: { type: object } }
type: object
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
patch:
tags:
- Services
summary: 'Update Application'
description: 'Update service application by UUID. Allows updating application settings including domains, image, and configuration flags.'
operationId: update-application-by-service-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the service.'
required: true
schema:
type: string
format: uuid
-
name: app_uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Application data to update.'
required: true
content:
application/json:
schema:
properties:
fqdn:
type: string
description: 'Fully qualified domain name with optional port. Multiple domains can be comma-separated.'
example: 'app.example.com:8080'
human_name:
type: string
description: 'Human-readable name for the application.'
example: 'My Application'
description:
type: string
description: 'Description of the application.'
example: 'Production web application'
image:
type: string
description: 'Docker image to use. WARNING: Changing the image may corrupt data.'
example: 'nginx:latest'
exclude_from_status:
type: boolean
description: 'Exclude this application from service status monitoring.'
example: false
is_log_drain_enabled:
type: boolean
description: 'Enable log draining to configured endpoint. Server must have log drain enabled.'
example: false
is_gzip_enabled:
type: boolean
description: 'Enable gzip compression in proxy.'
example: true
is_stripprefix_enabled:
type: boolean
description: 'Enable path prefix stripping in proxy.'
example: true
type: object
responses:
'200':
description: 'Application updated successfully.'
content:
application/json:
schema:
properties:
uuid: { type: string, description: 'Application UUID.' }
name: { type: string, description: 'Application internal name.' }
human_name: { type: string, description: 'Human-readable name.' }
description: { type: string, description: 'Application description.' }
fqdn: { type: string, description: 'Application domains.' }
image: { type: string, description: 'Docker image.' }
exclude_from_status: { type: boolean, description: 'Status monitoring exclusion.' }
is_log_drain_enabled: { type: boolean, description: 'Log drain status.' }
is_gzip_enabled: { type: boolean, description: 'Gzip compression status.' }
is_stripprefix_enabled: { type: boolean, description: 'Strip prefix status.' }
message: { type: string, example: 'Application updated successfully. Restart the service to apply changes.' }
type: object
'400':
$ref: '#/components/responses/400'
'401':
$ref: '#/components/responses/401'
'404':
$ref: '#/components/responses/404'
'409':
description: 'Domain conflict detected.'
content:
application/json:
schema:
properties:
message: { type: string, example: 'Domain conflict detected.' }
conflicts: { type: array, items: { type: object } }
type: object
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
🧰 Tools
🪛 YAMLlint (1.37.1)

[error] 5667-5667: too many spaces inside braces

(braces)


[error] 5667-5667: too many spaces inside braces

(braces)


[error] 5668-5668: too many spaces inside braces

(braces)


[error] 5668-5668: too many spaces inside braces

(braces)


[error] 5669-5669: too many spaces inside braces

(braces)


[error] 5669-5669: too many spaces inside braces

(braces)


[error] 5670-5670: too many spaces inside braces

(braces)


[error] 5670-5670: too many spaces inside braces

(braces)


[error] 5671-5671: too many spaces inside braces

(braces)


[error] 5671-5671: too many spaces inside braces

(braces)


[error] 5672-5672: too many spaces inside braces

(braces)


[error] 5672-5672: too many spaces inside braces

(braces)


[error] 5673-5673: too many spaces inside braces

(braces)


[error] 5673-5673: too many spaces inside braces

(braces)


[error] 5674-5674: too many spaces inside braces

(braces)


[error] 5674-5674: too many spaces inside braces

(braces)


[error] 5675-5675: too many spaces inside braces

(braces)


[error] 5675-5675: too many spaces inside braces

(braces)


[error] 5676-5676: too many spaces inside braces

(braces)


[error] 5676-5676: too many spaces inside braces

(braces)


[error] 5677-5677: too many spaces inside braces

(braces)


[error] 5677-5677: too many spaces inside braces

(braces)


[error] 5689-5689: too many spaces inside braces

(braces)


[error] 5689-5689: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)


[error] 5690-5690: too many spaces inside braces

(braces)

🤖 Prompt for AI Agents
In openapi.yaml around lines 5597 to 5696, the PATCH operation responses lack a
400 Bad Request entry; add a '400' response for consistency (preferably
referencing an existing reusable response such as $ref:
'#/components/responses/400' or defining a simple application/json schema with
message and errors) under the responses block so clients can expect and handle
validation/Bad Request responses consistently.

Comment on lines 61 to 71
test('returns 404 when application not found', function () {
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson(
'/api/v1/services/service-uuid/applications/non-existent-uuid',
['fqdn' => 'test.example.com']
);

$response->assertStatus(404);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test does not verify application-specific 404 scenario.

I'll be back... to tell you this test will always return 404 for "Service not found" since service-uuid doesn't exist in the database. The application lookup never executes. Like trying to find tacos in a serverless kitchen - you can't find what doesn't exist at the infrastructure level first.

To properly test "application not found", create an actual Service first:

 test('returns 404 when application not found', function () {
+    $service = \App\Models\Service::factory()->create([
+        'environment_id' => \App\Models\Environment::factory()->create([
+            'project_id' => \App\Models\Project::factory()->create(['team_id' => $this->team->id])->id,
+        ])->id,
+    ]);
+
     $response = $this->withHeaders([
         'Authorization' => 'Bearer '.$this->bearerToken,
         'Content-Type' => 'application/json',
     ])->patchJson(
-        '/api/v1/services/service-uuid/applications/non-existent-uuid',
+        '/api/v1/services/'.$service->uuid.'/applications/non-existent-uuid',
         ['fqdn' => 'test.example.com']
     );

     $response->assertStatus(404);
+    $response->assertJson(['message' => 'Application not found.']);
 });
📝 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
test('returns 404 when application not found', function () {
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson(
'/api/v1/services/service-uuid/applications/non-existent-uuid',
['fqdn' => 'test.example.com']
);
$response->assertStatus(404);
});
test('returns 404 when application not found', function () {
$service = \App\Models\Service::factory()->create([
'environment_id' => \App\Models\Environment::factory()->create([
'project_id' => \App\Models\Project::factory()->create(['team_id' => $this->team->id])->id,
])->id,
]);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
'Content-Type' => 'application/json',
])->patchJson(
'/api/v1/services/'.$service->uuid.'/applications/non-existent-uuid',
['fqdn' => 'test.example.com']
);
$response->assertStatus(404);
$response->assertJson(['message' => 'Application not found.']);
});
🤖 Prompt for AI Agents
In tests/Feature/ServiceApplicationApiTest.php around lines 61-71, the test uses
a non-existent service id so the request fails with "Service not found" before
the application lookup; update the test to create a real Service (using a
factory or seeder) and use its UUID in the request URL, then call PATCH against
a non-existent application UUID to assert a 404 for "Application not found".
Ensure the created Service is persisted to the test database, keep the same
headers and payload, and assert the 404 response after targeting the existing
service UUID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants