mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(api): accept github_app_uuid when updating an application
Closes #12016
This commit is contained in:
@@ -3063,14 +3063,10 @@ class ApplicationsController extends Controller
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
|
||||
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
|
||||
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'],
|
||||
'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
|
||||
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
|
||||
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
|
||||
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
|
||||
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'],
|
||||
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The application name.'],
|
||||
'description' => ['type' => 'string', 'description' => 'The application description.'],
|
||||
@@ -3262,10 +3258,11 @@ class ApplicationsController extends Controller
|
||||
$dnsCleanup = app(ManagedDnsRecordCleanup::class);
|
||||
$previousDnsHostnames = $dnsCleanup->hostnamesOf($application);
|
||||
$server = $application->destination->server;
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'use_build_secrets', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', ...self::APPLICATION_SETTING_FIELDS];
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'is_preview_deployments_enabled', 'domains', 'noindex_domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'dockerfile_target_build', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'use_build_secrets', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled', 'is_preserve_repository_enabled', 'preview_url_template', 'max_restart_count', 'github_app_uuid', ...self::APPLICATION_SETTING_FIELDS];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
'github_app_uuid' => 'string',
|
||||
'description' => 'string|nullable',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
@@ -3342,6 +3339,24 @@ class ApplicationsController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$githubApp = null;
|
||||
if ($request->has('github_app_uuid')) {
|
||||
if ($application->build_pack === 'dockerimage' || $application->deploymentType() === 'deploy_key') {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => [
|
||||
'github_app_uuid' => 'The github_app_uuid can only be changed for applications deployed from a GitHub App or public repository.',
|
||||
],
|
||||
], 422);
|
||||
}
|
||||
$githubApp = GithubApp::where('uuid', $request->github_app_uuid)
|
||||
->where(fn ($query) => $query->where('team_id', $teamId)->orWhere('is_system_wide', true))
|
||||
->first();
|
||||
if (! $githubApp?->isConnected()) {
|
||||
return response()->json(['message' => 'Github App not found.'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$applicationSettings = $this->applicationSettingsFromRequest($request);
|
||||
if ($prefixValidation = $this->containerNamePrefixValidationResponse($applicationSettings, $application->destination->server, $application)) {
|
||||
return $prefixValidation;
|
||||
@@ -3595,6 +3610,12 @@ class ApplicationsController extends Controller
|
||||
$requestHasNoindexDomains = $request->has('noindex_domains');
|
||||
data_forget($data, 'noindex_domains');
|
||||
$application->fill($data);
|
||||
if ($githubApp && ($application->source_id !== $githubApp->id || $application->source_type !== $githubApp->getMorphClass())) {
|
||||
$application->source_id = $githubApp->id;
|
||||
$application->source_type = $githubApp->getMorphClass();
|
||||
// The next deployment fetches the repository ID from the new source.
|
||||
$application->repository_project_id = null;
|
||||
}
|
||||
if ($requestHasNoindexDomains) {
|
||||
// Must run after fqdn is filled: flags are kept only for domains the app still has.
|
||||
$application->setNoindexDomains($request->input('noindex_domains') ?? []);
|
||||
|
||||
@@ -3416,18 +3416,6 @@
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"project_uuid": {
|
||||
"type": "string",
|
||||
"description": "The project UUID."
|
||||
},
|
||||
"server_uuid": {
|
||||
"type": "string",
|
||||
"description": "The server UUID."
|
||||
},
|
||||
"environment_name": {
|
||||
"type": "string",
|
||||
"description": "The environment name."
|
||||
},
|
||||
"github_app_uuid": {
|
||||
"type": "string",
|
||||
"description": "The Github App UUID."
|
||||
@@ -3444,10 +3432,6 @@
|
||||
"type": "string",
|
||||
"description": "The ports to expose."
|
||||
},
|
||||
"destination_uuid": {
|
||||
"type": "string",
|
||||
"description": "The destination UUID."
|
||||
},
|
||||
"build_pack": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -2239,15 +2239,6 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
project_uuid:
|
||||
type: string
|
||||
description: 'The project UUID.'
|
||||
server_uuid:
|
||||
type: string
|
||||
description: 'The server UUID.'
|
||||
environment_name:
|
||||
type: string
|
||||
description: 'The environment name.'
|
||||
github_app_uuid:
|
||||
type: string
|
||||
description: 'The Github App UUID.'
|
||||
@@ -2260,9 +2251,6 @@ paths:
|
||||
ports_exposes:
|
||||
type: string
|
||||
description: 'The ports to expose.'
|
||||
destination_uuid:
|
||||
type: string
|
||||
description: 'The destination UUID.'
|
||||
build_pack:
|
||||
type: string
|
||||
enum: [nixpacks, railpack, static, dockerfile, dockercompose]
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$this->token = $this->user->createToken('github-app-source-test', ['*'])->plainTextToken;
|
||||
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
|
||||
$this->oldGithubApp = githubAppSourceApiApp($this->team->id, 'org-a');
|
||||
$this->newGithubApp = githubAppSourceApiApp($this->team->id, 'org-b');
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'git_repository' => 'org-a/repository',
|
||||
'source_id' => $this->oldGithubApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
'repository_project_id' => 111,
|
||||
'private_key_id' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
function githubAppSourceApiApp(int $teamId, string $name, array $overrides = []): GithubApp
|
||||
{
|
||||
return GithubApp::create(array_merge([
|
||||
'name' => $name,
|
||||
'team_id' => $teamId,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'app_id' => 1,
|
||||
'installation_id' => 1,
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
function patchApplicationSource(array $payload)
|
||||
{
|
||||
return test()->withHeaders(['Authorization' => 'Bearer '.test()->token])
|
||||
->patchJson('/api/v1/applications/'.test()->application->uuid, $payload);
|
||||
}
|
||||
|
||||
test('PATCH moves the application to another GitHub App of the team', function () {
|
||||
patchApplicationSource([
|
||||
'github_app_uuid' => $this->newGithubApp->uuid,
|
||||
'git_repository' => 'org-b/repository',
|
||||
'git_branch' => 'main',
|
||||
])->assertOk();
|
||||
|
||||
$application = $this->application->refresh();
|
||||
expect($application->source_id)->toBe($this->newGithubApp->id)
|
||||
->and($application->source_type)->toBe(GithubApp::class)
|
||||
->and($application->git_repository)->toBe('org-b/repository')
|
||||
->and($application->repository_project_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('PATCH rejects a GitHub App of another team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$foreignGithubApp = githubAppSourceApiApp($otherTeam->id, 'foreign');
|
||||
|
||||
patchApplicationSource(['github_app_uuid' => $foreignGithubApp->uuid])->assertNotFound();
|
||||
|
||||
expect($this->application->refresh()->source_id)->toBe($this->oldGithubApp->id);
|
||||
});
|
||||
|
||||
test('PATCH rejects a GitHub App that is not installed', function () {
|
||||
$notInstalled = githubAppSourceApiApp($this->team->id, 'not-installed', ['installation_id' => null]);
|
||||
|
||||
patchApplicationSource(['github_app_uuid' => $notInstalled->uuid])->assertNotFound();
|
||||
|
||||
expect($this->application->refresh()->source_id)->toBe($this->oldGithubApp->id);
|
||||
});
|
||||
|
||||
test('PATCH rejects github_app_uuid for deploy key applications', function () {
|
||||
$this->application->forceFill(['private_key_id' => 1])->saveQuietly();
|
||||
|
||||
patchApplicationSource(['github_app_uuid' => $this->newGithubApp->uuid])
|
||||
->assertUnprocessable()
|
||||
->assertInvalid(['github_app_uuid']);
|
||||
|
||||
expect($this->application->refresh()->source_id)->toBe($this->oldGithubApp->id);
|
||||
});
|
||||
Reference in New Issue
Block a user