mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-28 09:34:36 +08:00
fix(applications): keep the base directory in new repository apps
The deploy key flow saved the base directory only for Docker Compose. All repository flows now validate the base directory, and the GitLab and Gitea flows reject destinations on servers that cannot deploy. Closes #10319
This commit is contained in:
1 parent
bbddf9543d
commit
af7365ad57
7 files changed
+245
-7
No files matched your search
@@ -170,10 +170,12 @@ class GiteaPrivateRepository extends Component
|
||||
'selected_repository_path' => $this->selected_repository_path,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
'base_directory' => $this->base_directory,
|
||||
], [
|
||||
'selected_repository_path' => 'required|string',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
'base_directory' => ValidationPatterns::directoryPathRules(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -181,7 +183,7 @@ class GiteaPrivateRepository extends Component
|
||||
}
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
|
||||
@@ -209,11 +209,13 @@ class GithubPrivateRepository extends Component
|
||||
'selected_repository_repo' => $this->selected_repository_repo,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
'base_directory' => $this->base_directory,
|
||||
], [
|
||||
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
|
||||
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
'base_directory' => ValidationPatterns::directoryPathRules(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
||||
@@ -40,9 +40,9 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
|
||||
public ?string $publish_directory = null;
|
||||
|
||||
// In case of docker compose
|
||||
public ?string $base_directory = null;
|
||||
public ?string $base_directory = '/';
|
||||
|
||||
// In case of docker compose
|
||||
public ?string $docker_compose_location = '/docker-compose.yaml';
|
||||
// End of docker compose
|
||||
|
||||
@@ -71,6 +71,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'is_static' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => ValidationPatterns::directoryPathRules(),
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
];
|
||||
}
|
||||
@@ -82,6 +83,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'is_static' => 'Is static',
|
||||
'publish_directory' => 'Publish directory',
|
||||
'build_pack' => 'Build pack',
|
||||
'base_directory' => 'Base directory',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -181,6 +183,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
'private_key_id' => $privateKey->id,
|
||||
'base_directory' => $this->base_directory,
|
||||
];
|
||||
} else {
|
||||
$application_init = [
|
||||
@@ -196,6 +199,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
'private_key_id' => $privateKey->id,
|
||||
'source_id' => $this->git_source->id,
|
||||
'source_type' => $this->git_source->getMorphClass(),
|
||||
'base_directory' => $this->base_directory,
|
||||
];
|
||||
}
|
||||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
@@ -206,7 +210,6 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = new Application($application_init);
|
||||
$application->save();
|
||||
|
||||
@@ -167,10 +167,12 @@ class GitlabPrivateRepository extends Component
|
||||
'selected_repository_path' => $this->selected_repository_path,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
'base_directory' => $this->base_directory,
|
||||
], [
|
||||
'selected_repository_path' => 'required|string',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
'base_directory' => ValidationPatterns::directoryPathRules(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -178,7 +180,7 @@ class GitlabPrivateRepository extends Component
|
||||
}
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
$destination = find_resource_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class PublicGitRepository extends Component
|
||||
'isStatic' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => 'nullable|string',
|
||||
'base_directory' => ValidationPatterns::directoryPathRules(),
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||
];
|
||||
@@ -352,7 +352,6 @@ class PublicGitRepository extends Component
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
$application = new Application($application_init);
|
||||
$application->save();
|
||||
|
||||
@@ -113,6 +113,51 @@ test('deploy key application keeps an scp-style ssh url with a custom username',
|
||||
->toBe('custom-user@github.example.com:organization/repository.git');
|
||||
});
|
||||
|
||||
test('deploy key application keeps the base directory for every build pack', function (string $buildPack) {
|
||||
actAsDeployKeyUser($this->admin, $this->team);
|
||||
|
||||
deployKeyComponent()
|
||||
->call('setPrivateKey', $this->privateKey->id)
|
||||
->set('repository_url', 'git@example.com:owner/monorepo.git')
|
||||
->set('branch', 'main')
|
||||
->set('build_pack', $buildPack)
|
||||
->set('base_directory', '/apps/api')
|
||||
->call('submit')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Application::query()->sole()->base_directory)->toBe('/apps/api');
|
||||
})->with(['dockerfile', 'railpack', 'static', 'dockercompose']);
|
||||
|
||||
test('deploy key application uses the repository root when the base directory is empty', function () {
|
||||
actAsDeployKeyUser($this->admin, $this->team);
|
||||
|
||||
deployKeyComponent()
|
||||
->call('setPrivateKey', $this->privateKey->id)
|
||||
->set('repository_url', 'git@example.com:owner/repository.git')
|
||||
->set('branch', 'main')
|
||||
->set('build_pack', 'dockerfile')
|
||||
->set('base_directory', '')
|
||||
->call('submit')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Application::query()->sole()->base_directory)->toBe('/');
|
||||
});
|
||||
|
||||
test('deploy key application rejects an invalid base directory', function () {
|
||||
actAsDeployKeyUser($this->admin, $this->team);
|
||||
|
||||
deployKeyComponent()
|
||||
->call('setPrivateKey', $this->privateKey->id)
|
||||
->set('repository_url', 'git@example.com:owner/repository.git')
|
||||
->set('branch', 'main')
|
||||
->set('build_pack', 'dockerfile')
|
||||
->set('base_directory', '/apps; rm -rf /')
|
||||
->call('submit')
|
||||
->assertHasErrors('base_directory');
|
||||
|
||||
expect(Application::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('member cannot invoke the deploy key selection action directly', function () {
|
||||
actAsDeployKeyUser($this->member, $this->team);
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ServerRole;
|
||||
use App\Livewire\Project\New\GiteaPrivateRepository;
|
||||
use App\Livewire\Project\New\GithubPrivateRepository;
|
||||
use App\Livewire\Project\New\GitlabPrivateRepository;
|
||||
use App\Livewire\Project\New\PublicGitRepository;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GiteaApp;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
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;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
$this->destination = StandaloneDocker::factory()->create([
|
||||
'server_id' => $this->server->id,
|
||||
'name' => 'dest-flows',
|
||||
'network' => 'coolify-flows',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
function repositoryFlowTarget(Testable $component): Testable
|
||||
{
|
||||
return $component
|
||||
->set('parameters', ['project_uuid' => test()->project->uuid, 'environment_uuid' => test()->environment->uuid])
|
||||
->set('query', ['destination' => test()->destination->uuid]);
|
||||
}
|
||||
|
||||
function publicRepositoryFlow(): Testable
|
||||
{
|
||||
return repositoryFlowTarget(Livewire::test(PublicGitRepository::class, ['type' => 'public']))
|
||||
->set('repository_url', 'https://github.com/acme/web')
|
||||
->set('git_repository', 'https://github.com/acme/web')
|
||||
->set('git_branch', 'main');
|
||||
}
|
||||
|
||||
function githubAppRepositoryFlow(): Testable
|
||||
{
|
||||
openssl_pkey_export(openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]), $pemKey);
|
||||
$githubApp = GithubApp::create([
|
||||
'name' => 'GitHub App',
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
'app_id' => 12345,
|
||||
'installation_id' => 67890,
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'client-secret',
|
||||
'webhook_secret' => 'webhook-secret',
|
||||
'private_key_id' => PrivateKey::create(['name' => 'Key', 'private_key' => $pemKey, 'team_id' => test()->team->id])->id,
|
||||
'team_id' => test()->team->id,
|
||||
'is_system_wide' => false,
|
||||
]);
|
||||
Http::fake([
|
||||
'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]),
|
||||
'https://api.github.com/app/installations/67890/access_tokens' => Http::response(['token' => 'installation-token'], 201),
|
||||
'https://api.github.com/installation/repositories*' => Http::response([
|
||||
'total_count' => 1,
|
||||
'repositories' => [['id' => 1, 'name' => 'web', 'owner' => ['login' => 'acme']]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
return repositoryFlowTarget(Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']))
|
||||
->call('loadRepositories', $githubApp->id)
|
||||
->set('selected_repository_owner', 'acme')
|
||||
->set('selected_repository_repo', 'web')
|
||||
->set('selected_branch_name', 'main');
|
||||
}
|
||||
|
||||
function gitlabRepositoryFlow(): Testable
|
||||
{
|
||||
$gitlabApp = GitlabApp::create([
|
||||
'name' => 'GitLab',
|
||||
'api_url' => 'https://1.1.1.1/api/v4',
|
||||
'html_url' => 'https://1.1.1.1',
|
||||
'custom_user' => 'git',
|
||||
'custom_port' => 22,
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'client-secret',
|
||||
'webhook_token' => 'webhook-token',
|
||||
'access_token' => 'access-token',
|
||||
'refresh_token' => 'refresh-token',
|
||||
'expires_at' => time() + 3600,
|
||||
'team_id' => test()->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
Http::fake(['1.1.1.1/api/v4/*' => Http::response([['name' => 'main']], 200)]);
|
||||
|
||||
return repositoryFlowTarget(Livewire::test(GitlabPrivateRepository::class, ['type' => 'private-gitlab-app']))
|
||||
->set('gitlab_app_id', $gitlabApp->id)
|
||||
->set('selected_project_id', 42)
|
||||
->set('selected_repository_path', 'acme/web')
|
||||
->set('selected_branch_name', 'main');
|
||||
}
|
||||
|
||||
function giteaRepositoryFlow(): Testable
|
||||
{
|
||||
// Http::GitSource() pins DNS before Http::fake() answers, so use an IP literal.
|
||||
$giteaApp = GiteaApp::create([
|
||||
'name' => 'Gitea',
|
||||
'api_url' => 'https://1.1.1.1/api/v1',
|
||||
'html_url' => 'https://1.1.1.1',
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'client-secret',
|
||||
'access_token' => 'access-token',
|
||||
'refresh_token' => 'refresh-token',
|
||||
'expires_at' => time() + 3600,
|
||||
'team_id' => test()->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
Http::fake([
|
||||
'1.1.1.1/api/v1/user/repos*' => Http::response([
|
||||
['id' => 42, 'name' => 'web', 'full_name' => 'acme/web', 'default_branch' => 'main', 'html_url' => 'https://1.1.1.1/acme/web', 'owner' => ['login' => 'acme']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
return repositoryFlowTarget(Livewire::test(GiteaPrivateRepository::class, ['type' => 'private-gitea-app']))
|
||||
->call('loadRepositories', $giteaApp->id)
|
||||
->set('selected_branch_name', 'main');
|
||||
}
|
||||
|
||||
dataset('repository flows', [
|
||||
'public repository' => 'publicRepositoryFlow',
|
||||
'github app' => 'githubAppRepositoryFlow',
|
||||
'gitlab' => 'gitlabRepositoryFlow',
|
||||
'gitea' => 'giteaRepositoryFlow',
|
||||
]);
|
||||
|
||||
test('repository flows keep the entered base directory', function (string $flow) {
|
||||
$flow()
|
||||
->set('build_pack', 'dockerfile')
|
||||
->set('base_directory', '/apps/api')
|
||||
->call('submit');
|
||||
|
||||
expect(Application::query()->sole()->base_directory)->toBe('/apps/api');
|
||||
})->with('repository flows');
|
||||
|
||||
test('repository flows reject an invalid base directory', function (string $flow) {
|
||||
$flow()
|
||||
->set('build_pack', 'dockerfile')
|
||||
->set('base_directory', '/apps; rm -rf /')
|
||||
->call('submit');
|
||||
|
||||
expect(Application::query()->count())->toBe(0);
|
||||
})->with('repository flows');
|
||||
|
||||
test('repository flows do not create an application on a build-only server', function (string $flow) {
|
||||
$this->server->settings->update(['server_role' => ServerRole::BUILD]);
|
||||
Server::flushIdentityMap();
|
||||
|
||||
$flow()->call('submit');
|
||||
|
||||
expect(Application::query()->count())->toBe(0);
|
||||
})->with('repository flows');
|
||||
Reference in new issue
Block a user