Merge branch 'next' into docker-image-login

This commit is contained in:
🏔️ Peak 2024-12-06 14:31:55 +01:00 committed by GitHub
commit d529f8f8aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 682 additions and 189 deletions

View file

@ -2,6 +2,7 @@
namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Events\ProxyStarted;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@ -37,11 +38,16 @@ class StartProxy
"echo 'Successfully started coolify-proxy.'",
]);
} else {
$caddfile = 'import /dynamic/*.caddy';
if (isDev()) {
if ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = '/data/coolify/proxy/caddy';
}
}
$caddyfile = 'import /dynamic/*.caddy';
$commands = $commands->merge([
"mkdir -p $proxy_path/dynamic",
"cd $proxy_path",
"echo '$caddfile' > $proxy_path/dynamic/Caddyfile",
"echo '$caddyfile' > $proxy_path/dynamic/Caddyfile",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',

View file

@ -211,8 +211,9 @@ class DatabasesController extends Controller
'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'],
'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'],
'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mongo_initdb_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@ -241,7 +242,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@ -413,12 +414,12 @@ class DatabasesController extends Controller
}
break;
case 'standalone-mongodb':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
if ($request->has('mongo_conf')) {
if (! isBase64Encoded($request->mongo_conf)) {
@ -443,9 +444,10 @@ class DatabasesController extends Controller
break;
case 'standalone-mysql':
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@ -909,6 +911,7 @@ class DatabasesController extends Controller
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
@ -1013,7 +1016,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@ -1220,9 +1223,10 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string',
'mysql_password' => 'string',
'mysql_user' => 'string',
'mysql_database' => 'string',
'mysql_conf' => 'string',
@ -1456,12 +1460,12 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [
'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string',
'mongo_initdb_root_password' => 'string',
'mongo_initdb_init_database' => 'string',
'mongo_initdb_database' => 'string',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {

View file

@ -463,7 +463,7 @@ class Github extends Controller
$private_key = data_get($data, 'pem');
$webhook_secret = data_get($data, 'webhook_secret');
$private_key = PrivateKey::create([
'name' => $slug,
'name' => "github-app-{$slug}",
'private_key' => $private_key,
'team_id' => $github_app->team_id,
'is_git_related' => true,

View file

@ -141,6 +141,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $buildTarget = null;
private bool $disableBuildCache = false;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
@ -179,7 +181,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit;
$this->rollback = $this->application_deployment_queue->rollback;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
if ($this->disableBuildCache) {
$this->force_rebuild = true;
}
$this->restart_only = $this->application_deployment_queue->restart_only;
$this->restart_only = $this->restart_only && $this->application->build_pack !== 'dockerimage' && $this->application->build_pack !== 'dockerfile';
$this->only_this_server = $this->application_deployment_queue->only_this_server;
@ -1998,6 +2004,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->build_args = $this->build_args->implode(' ');
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if ($this->disableBuildCache) {
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
}
if ($this->application->build_pack === 'static') {
$this->application_deployment_queue->addLogEntry('Static deployment. Copying static assets to the image.');
} else {

View file

@ -14,7 +14,7 @@ class ProxyStartedNotification
public function handle(ProxyStarted $event): void
{
$this->server = data_get($event, 'data');
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->server->setupDynamicProxyConfiguration();
$this->server->proxy->force_stop = false;
$this->server->save();

View file

@ -37,7 +37,7 @@ class Email extends Component
#[Validate(['nullable', 'numeric'])]
public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])]
#[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])]

View file

@ -25,6 +25,9 @@ class Advanced extends Component
#[Validate(['boolean'])]
public bool $isAutoDeployEnabled = true;
#[Validate(['boolean'])]
public bool $disableBuildCache = false;
#[Validate(['boolean'])]
public bool $isLogDrainEnabled = false;
@ -95,6 +98,7 @@ class Advanced extends Component
$this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled;
$this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled;
$this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled;
$this->application->settings->disable_build_cache = $this->disableBuildCache;
$this->application->settings->save();
} else {
$this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled();
@ -116,6 +120,7 @@ class Advanced extends Component
$this->customInternalName = $this->application->settings->custom_internal_name;
$this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled;
$this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network;
$this->disableBuildCache = $this->application->settings->disable_build_cache;
}
}

View file

@ -168,18 +168,42 @@ class ExecuteContainerCommand extends Component
return;
}
try {
// Validate container name format
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $this->selected_container)) {
throw new \InvalidArgumentException('Invalid container name format');
}
// Verify container exists in our allowed list
$container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
if (is_null($container)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($this->container, 'server');
// Verify server ownership and status
$server = data_get($container, 'server');
if (! $server || ! $server instanceof Server) {
throw new \RuntimeException('Invalid server configuration.');
}
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
// Additional ownership verification based on resource type
$resourceServer = match ($this->type) {
'application' => $this->resource->destination->server,
'database' => $this->resource->destination->server,
'service' => $this->resource->server,
default => throw new \RuntimeException('Invalid resource type.')
};
if ($server->id !== $resourceServer->id && ! $this->resource->additional_servers->contains('id', $server->id)) {
throw new \RuntimeException('Server ownership verification failed.');
}
$this->dispatch(
'send-terminal-command',
isset($container),
true,
data_get($container, 'container.Names'),
data_get($container, 'server.uuid')
);

View file

@ -29,11 +29,20 @@ class Terminal extends Component
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) {
// Validate container identifier format (alphanumeric, dashes, and underscores only)
if (! preg_match('/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/', $identifier)) {
throw new \InvalidArgumentException('Invalid container identifier format');
}
// Verify container exists and belongs to the user's team
$status = getContainerStatus($server, $identifier);
if ($status !== 'running') {
return;
}
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
// Escape the identifier for shell usage
$escapedIdentifier = escapeshellarg($identifier);
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
}

View file

@ -15,6 +15,8 @@ class Proxy extends Component
public $proxy_settings = null;
public bool $redirect_enabled = true;
public ?string $redirect_url = null;
protected $listeners = ['proxyStatusUpdated', 'saveConfiguration' => 'submit'];
@ -26,6 +28,7 @@ class Proxy extends Component
public function mount()
{
$this->selectedProxy = $this->server->proxyType();
$this->redirect_enabled = data_get($this->server, 'proxy.redirect_enabled', true);
$this->redirect_url = data_get($this->server, 'proxy.redirect_url');
}
@ -38,7 +41,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyChanged');
$this->dispatch('reloadWindow');
}
public function selectProxy($proxy_type)
@ -46,7 +49,7 @@ class Proxy extends Component
try {
$this->server->changeProxy($proxy_type, async: false);
$this->selectedProxy = $this->server->proxy->type;
$this->dispatch('proxyStatusUpdated');
$this->dispatch('reloadWindow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@ -63,13 +66,25 @@ class Proxy extends Component
}
}
public function instantSaveRedirect()
{
try {
$this->server->proxy->redirect_enabled = $this->redirect_enabled;
$this->server->save();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {
SaveConfiguration::run($this->server, $this->proxy_settings);
$this->server->proxy->redirect_url = $this->redirect_url;
$this->server->save();
$this->server->setupDefault404Redirect();
$this->server->setupDefaultRedirect();
$this->dispatch('success', 'Proxy configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);

View file

@ -65,7 +65,7 @@ class Deploy extends Component
public function restart()
{
try {
$this->stop(forceStop: false);
$this->stop();
$this->dispatch('checkProxy');
} catch (\Throwable $e) {
return handleError($e, $this);
@ -105,6 +105,7 @@ class Deploy extends Component
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
ray('running');
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;

View file

@ -5,7 +5,7 @@ namespace App\Livewire\Server;
use App\Actions\Server\StartSentinel;
use App\Actions\Server\StopSentinel;
use App\Models\Server;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@ -79,9 +79,6 @@ class Show extends Component
#[Validate(['required'])]
public string $serverTimezone;
#[Locked]
public array $timezones;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@ -96,13 +93,21 @@ class Show extends Component
{
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail();
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->syncData();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
#[Computed]
public function timezones(): array
{
return collect(timezone_identifiers_list())
->sort()
->values()
->toArray();
}
public function syncData(bool $toModel = false)
{
if ($toModel) {

View file

@ -7,7 +7,7 @@ use App\Models\InstanceSettings;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Validate;
use Livewire\Component;
@ -17,9 +17,6 @@ class Index extends Component
protected Server $server;
#[Locked]
public $timezones;
#[Validate('boolean')]
public bool $is_auto_update_enabled;
@ -101,12 +98,20 @@ class Index extends Component
$this->is_api_enabled = $this->settings->is_api_enabled;
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
$this->instance_timezone = $this->settings->instance_timezone;
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
}
}
#[Computed]
public function timezones(): array
{
return collect(timezone_identifiers_list())
->sort()
->values()
->toArray();
}
public function instantSave($isSave = true)
{
$this->validate();

View file

@ -19,7 +19,7 @@ class SettingsEmail extends Component
#[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])]
public ?int $smtpPort = null;
#[Validate(['nullable', 'string'])]
#[Validate(['nullable', 'string', 'in:tls,ssl,none'])]
public ?string $smtpEncryption = null;
#[Validate(['nullable', 'string'])]

View file

@ -4,6 +4,11 @@ namespace App\Livewire\Source\Github;
use App\Jobs\GithubAppPermissionJob;
use App\Models\GithubApp;
use App\Models\PrivateKey;
use Illuminate\Support\Facades\Http;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Livewire\Component;
class Change extends Component
@ -51,12 +56,20 @@ class Change extends Component
'github_app.administration' => 'nullable|string',
];
public function boot()
{
if ($this->github_app) {
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
}
}
public function checkPermissions()
{
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
}
// public function check()
// {
@ -90,15 +103,16 @@ class Change extends Component
// ray($runners_by_repository);
// }
public function mount()
{
try {
$github_app_uuid = request()->github_app_uuid;
$this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail();
$this->github_app->makeVisible(['client_secret', 'webhook_secret']);
$this->applications = $this->github_app->applications;
$settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab();
$this->fqdn = $settings->fqdn;
@ -142,6 +156,77 @@ class Change extends Component
}
}
public function getGithubAppNameUpdatePath()
{
if (str($this->github_app->organization)->isNotEmpty()) {
return "{$this->github_app->html_url}/organizations/{$this->github_app->organization}/settings/apps/{$this->github_app->name}";
}
return "{$this->github_app->html_url}/settings/apps/{$this->github_app->name}";
}
private function generateGithubJwt($private_key, $app_id): string
{
$configuration = Configuration::forAsymmetricSigner(
new Sha256,
InMemory::plainText($private_key),
InMemory::plainText($private_key)
);
$now = time();
return $configuration->builder()
->issuedBy((string) $app_id)
->permittedFor('https://api.github.com')
->identifiedBy((string) $now)
->issuedAt(new \DateTimeImmutable("@{$now}"))
->expiresAt(new \DateTimeImmutable('@'.($now + 600)))
->getToken($configuration->signer(), $configuration->signingKey())
->toString();
}
public function updateGithubAppName()
{
try {
$privateKey = PrivateKey::ownedByCurrentTeam()->find($this->github_app->private_key_id);
if (! $privateKey) {
$this->dispatch('error', 'No private key found for this GitHub App.');
return;
}
$jwt = $this->generateGithubJwt($privateKey->private_key, $this->github_app->app_id);
$response = Http::withHeaders([
'Accept' => 'application/vnd.github+json',
'X-GitHub-Api-Version' => '2022-11-28',
'Authorization' => "Bearer {$jwt}",
])->get("{$this->github_app->api_url}/app");
if ($response->successful()) {
$app_data = $response->json();
$app_slug = $app_data['slug'] ?? null;
if ($app_slug) {
$this->github_app->name = $app_slug;
$this->name = str($app_slug)->kebab();
$privateKey->name = "github-app-{$app_slug}";
$privateKey->save();
$this->github_app->save();
$this->dispatch('success', 'GitHub App name and SSH key name synchronized successfully.');
} else {
$this->dispatch('info', 'Could not find App Name (slug) in GitHub response.');
}
} else {
$error_message = $response->json()['message'] ?? 'Unknown error';
$this->dispatch('error', "Failed to fetch GitHub App information: {$error_message}");
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View file

@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
@ -106,7 +107,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
use SoftDeletes;
use HasFactory, SoftDeletes;
private static $parserVersion = '4';
@ -1327,17 +1328,43 @@ class Application extends BaseModel
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
if (version_compare($gitVersion, '2.35.1', '<')) {
$fileList = $fileList->map(function ($file) {
$parts = explode('/', trim($file, '.'));
$paths = collect();
$currentPath = '';
foreach ($parts as $part) {
$currentPath .= ($currentPath ? '/' : '').$part;
$paths->push($currentPath);
}
return $paths;
})->flatten()->unique()->values();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
} else {
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
}
try {
$composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) {

View file

@ -11,6 +11,7 @@ use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@ -48,7 +49,7 @@ use Symfony\Component\Yaml\Yaml;
class Server extends BaseModel
{
use SchemalessAttributesTrait, SoftDeletes;
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@ -104,6 +105,14 @@ class Server extends BaseModel
]);
}
}
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::retrieved(function ($server) {
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::forceDeleting(function ($server) {
@ -183,73 +192,80 @@ class Server extends BaseModel
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
public function setupDefault404Redirect()
public function setupDefaultRedirect()
{
$banner =
"# This file is generated by Coolify, do not edit it manually.\n".
"# Disable the default redirect to customize (only if you know what are you doing).\n\n";
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
$redirect_enabled = $this->proxy->redirect_enabled ?? true;
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if (isDev()) {
if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
$this->reloadCaddy();
return;
$dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $default_redirect_file",
], $this);
return;
}
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'HostRegexp(`.+`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp',
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy";
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $dynamic_conf_path/default_redirect_404.yaml",
"rm -f $dynamic_conf_path/default_redirect_404.caddy",
], $this);
if ($redirect_enabled === false) {
instant_remote_process(["rm -f $default_redirect_file"], $this);
} else {
if ($proxy_type === ProxyTypes::CADDY->value) {
if (filled($redirect_url)) {
$conf = ":80, :443 {
redir $redirect_url
}";
} else {
$conf = ':80, :443 {
respond 503
}';
}
} elseif ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'PathPrefix(`/`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => -1000,
],
],
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => '',
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [],
],
],
],
],
'middlewares' => [
];
if (filled($redirect_url)) {
$dynamic_conf['http']['routers']['catchall']['middlewares'] = [
0 => 'redirect-regexp',
];
$dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [
'url' => '',
];
$dynamic_conf['http']['middlewares'] = [
'redirect-regexp' => [
'redirectRegex' => [
'regex' => '(.*)',
@ -257,32 +273,17 @@ respond 404
'permanent' => false,
],
],
],
],
];
$conf = Yaml::dump($dynamic_conf, 12, 2);
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
];
}
$conf = Yaml::dump($dynamic_conf, 12, 2);
}
$conf = $banner.$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@ -610,7 +611,9 @@ $schema://$host {
}
$memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['usedPercent']];
$usedPercent = $metric['usedPercent'] ?? 0.0;
return [(int) $metric['time'], (float) $usedPercent];
});
return $parsedCollection->toArray();

View file

@ -66,11 +66,12 @@ class EmailChannel
'transport' => 'smtp',
'host' => data_get($notifiable, 'smtp_host'),
'port' => data_get($notifiable, 'smtp_port'),
'encryption' => data_get($notifiable, 'smtp_encryption'),
'encryption' => data_get($notifiable, 'smtp_encryption') === 'none' ? null : data_get($notifiable, 'smtp_encryption'),
'username' => data_get($notifiable, 'smtp_username'),
'password' => data_get($notifiable, 'smtp_password'),
'timeout' => data_get($notifiable, 'smtp_timeout'),
'local_domain' => null,
'auto_tls' => data_get($notifiable, 'smtp_encryption') === 'none' ? '0' : '',
]);
}
}

View file

@ -46,7 +46,7 @@ const SPECIFIC_SERVICES = [
// Based on /etc/os-release
const SUPPORTED_OS = [
'ubuntu debian raspbian',
'ubuntu debian raspbian pop',
'centos fedora rhel ol rocky amzn almalinux',
'sles opensuse-leap opensuse-tumbleweed',
'arch',

View file

@ -288,9 +288,9 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme();
$port = $url->getPort();
$handle = "handle_path";
if ( ! $is_stripprefix_enabled){
$handle = "handle";
$handle = 'handle_path';
if (! $is_stripprefix_enabled) {
$handle = 'handle';
}
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
@ -302,7 +302,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
if ($port) {
$labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}");
} else {

View file

@ -173,13 +173,12 @@ function generate_default_proxy_configuration(Server $server)
],
'volumes' => [
'/var/run/docker.sock:/var/run/docker.sock:ro',
"{$proxy_path}:/traefik",
],
'command' => [
'--ping=true',
'--ping.entrypoint=http',
'--api.dashboard=true',
'--api.insecure=false',
'--entrypoints.http.address=:80',
'--entrypoints.https.address=:443',
'--entrypoints.http.http.encodequerysemicolons=true',
@ -187,21 +186,26 @@ function generate_default_proxy_configuration(Server $server)
'--entrypoints.https.http.encodequerysemicolons=true',
'--entryPoints.https.http2.maxConcurrentStreams=50',
'--entrypoints.https.http3',
'--providers.docker.exposedbydefault=false',
'--providers.file.directory=/traefik/dynamic/',
'--providers.docker.exposedbydefault=false',
'--providers.file.watch=true',
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
'--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json',
],
'labels' => $labels,
],
],
];
if (isDev()) {
// $config['services']['traefik']['command'][] = "--log.level=debug";
$config['services']['traefik']['command'][] = '--api.insecure=true';
$config['services']['traefik']['command'][] = '--log.level=debug';
$config['services']['traefik']['command'][] = '--accesslog.filepath=/traefik/access.log';
$config['services']['traefik']['command'][] = '--accesslog.bufferingsize=100';
$config['services']['traefik']['volumes'][] = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/:/traefik';
} else {
$config['services']['traefik']['command'][] = '--api.insecure=false';
$config['services']['traefik']['volumes'][] = "{$proxy_path}:/traefik";
}
if ($server->isSwarm()) {
data_forget($config, 'services.traefik.container_name');

View file

@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.0.0-beta.375',
'version' => '4.0.0-beta.376',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),

View file

@ -49,6 +49,22 @@ return [
'search_path' => 'public',
'sslmode' => 'prefer',
],
'testing' => [
'driver' => 'pgsql',
'url' => env('DATABASE_TEST_URL'),
'host' => env('DB_TEST_HOST', 'postgres'),
'port' => env('DB_TEST_PORT', '5432'),
'database' => env('DB_TEST_DATABASE', 'coolify_test'),
'username' => env('DB_TEST_USERNAME', 'coolify'),
'password' => env('DB_TEST_PASSWORD', 'password'),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
],
/*

View file

@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ApplicationFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->name(),
'destination_id' => 1,
'git_repository' => fake()->url(),
'git_branch' => fake()->word(),
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
'environment_id' => 1,
'destination_id' => 1,
];
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class ServerFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->name(),
'ip' => fake()->unique()->ipv4(),
'private_key_id' => 1,
];
}
}

View file

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->boolean('disable_build_cache')->default(false);
});
}
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('disable_build_cache');
});
}
};

View file

@ -23,6 +23,7 @@ class GithubAppSeeder extends Seeder
GithubApp::create([
'name' => 'coolify-laravel-development-public',
'uuid' => '69420',
'organization' => 'coollabsio',
'api_url' => 'https://api.github.com',
'html_url' => 'https://github.com',
'is_public' => false,

View file

@ -3011,7 +3011,7 @@
"type": "string",
"description": "Mongo initdb root password"
},
"mongo_initdb_init_database": {
"mongo_initdb_database": {
"type": "string",
"description": "Mongo initdb init database"
},
@ -3019,6 +3019,10 @@
"type": "string",
"description": "MySQL root password"
},
"mysql_password": {
"type": "string",
"description": "MySQL password"
},
"mysql_user": {
"type": "string",
"description": "MySQL user"
@ -3842,6 +3846,10 @@
"type": "string",
"description": "MySQL root password"
},
"mysql_password": {
"type": "string",
"description": "MySQL password"
},
"mysql_user": {
"type": "string",
"description": "MySQL user"

View file

@ -2089,12 +2089,15 @@ paths:
mongo_initdb_root_password:
type: string
description: 'Mongo initdb root password'
mongo_initdb_init_database:
mongo_initdb_database:
type: string
description: 'Mongo initdb init database'
mysql_root_password:
type: string
description: 'MySQL root password'
mysql_password:
type: string
description: 'MySQL password'
mysql_user:
type: string
description: 'MySQL user'
@ -2684,6 +2687,9 @@ paths:
mysql_root_password:
type: string
description: 'MySQL root password'
mysql_password:
type: string
description: 'MySQL password'
mysql_user:
type: string
description: 'MySQL user'

View file

@ -13,8 +13,8 @@
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="DB_CONNECTION" value="testing"/>
<env name="DB_TEST_DATABASE" value="coolify_test"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>

11
public/svgs/heimdall.svg Normal file
View file

@ -0,0 +1,11 @@
<svg width="346" height="434" xmlns="http://www.w3.org/2000/svg">
<g>
<title>background</title>
<rect fill="none" id="canvas_background" height="436" width="348" y="-1" x="-1"/>
</g>
<g>
<title>Layer 1</title>
<path id="svg_1" d="m221.227,397.119l0,-24.919l8.552,-26.577c59.868,-59.323 106.907,-114.847 115.221,-131.932l-10.215,3.56s9.5,-11.864 8.552,-18.51c-7.6,-116.273 -73.17,-176.862 -91.226,-191.732c26.607,31.955 19.326,42.4 19.326,42.4c-11.723,-21.833 -35.797,-43.032 -40.549,-48.409l-5.7,33.853c22.806,13.289 16.79,20.882 16.79,20.882c-5.387,-7.594 -19.957,-14.235 -19.957,-14.235l-1.586,9.491c6.973,3.165 38.327,22.149 38.565,70.872c0,59.323 -33.735,74.985 -39.674,77.357c-19.718,-11.865 -41.1,-11.627 -46.326,-12.1c-5.226,0.474 -26.608,0.236 -46.324,12.1c-5.941,-2.376 -39.676,-18.038 -39.676,-77.361c0.238,-48.723 31.6,-67.707 38.564,-70.872l-1.581,-9.487s-14.574,6.644 -19.955,14.238c0,0 -6.022,-7.593 16.786,-20.882l-5.702,-33.856c-4.752,5.378 -28.824,26.577 -40.543,48.407c0,0 -7.288,-10.443 19.32,-42.4c-18.054,14.87 -83.625,75.459 -91.226,191.732c-0.95,6.646 8.554,18.51 8.554,18.51l-10.217,-3.558c8.316,17.085 55.355,72.612 115.221,131.935l8.554,26.577l0,24.916l-7.6,-26.577l-17.345,-28.475l0,28.95c7.127,20.169 34.921,60.983 34.921,60.983l0,-84.475l-106.431,-116.037s66.52,54.1 85.526,73.56l9.978,-10.2c-12.355,-8.78 -66.045,-62.646 -70.8,-110.1c40.388,85.426 79.111,96.1 107.62,129.325l0,8.127l24.707,0l0,-8.131c28.508,-33.221 67.233,-43.9 107.619,-129.325c-4.75,47.459 -58.442,101.325 -70.8,110.1l9.976,10.2c19.006,-19.458 85.526,-73.56 85.526,-73.56l-106.423,116.041l0,84.475s27.8,-40.815 34.923,-60.984l0,-28.95l-17.343,28.475l-7.602,26.578zm-48.227,-102.036c-17.818,-11.865 -63.668,-39.629 -82.672,-83.053a314.117,314.117 0 0 0 34.447,34.882s-1.189,-30.61 1.186,-37.255c4.276,36.781 13.068,52.442 47.039,71.9c33.973,-19.458 42.763,-35.119 47.039,-71.9c2.375,6.645 1.188,37.255 1.188,37.255a314.117,314.117 0 0 0 34.447,-34.882c-19.006,43.424 -64.856,71.188 -82.674,83.053z" class="cls-1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
public/svgs/kuzzle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/svgs/pairdrop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

3
public/svgs/penpot.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
public/svgs/whoogle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -7,6 +7,7 @@
'action' => 'delete',
'content' => null,
'closeOutside' => true,
'minWidth' => '36rem',
])
<div x-data="{ modalOpen: false }" :class="{ 'z-40': modalOpen }" @keydown.window.escape="modalOpen=false"
class="relative w-auto h-auto" wire:ignore>
@ -40,7 +41,7 @@
x-transition:leave="ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[36rem] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
class="relative w-full py-6 border rounded drop-shadow min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
<div class="flex items-center justify-between pb-3">
<h3 class="text-2xl font-bold">{{ $title }}</h3>
<button @click="modalOpen=false"

View file

@ -17,7 +17,8 @@
@if (isEmailEnabled($team) && auth()->user()->isAdminFromSession() && isTestEmailEnabled($team))
<x-modal-input buttonTitle="Send Test Email" title="Send Test Email">
<form wire:submit.prevent="sendTestEmail" class="flex flex-col w-full gap-2">
<x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress" label="Recipients" required />
<x-forms.input wire:model="testEmailAddress" placeholder="test@example.com" id="testEmailAddress"
label="Recipients" required />
<x-forms.button type="submit" @click="modalOpen=false">
Send Email
</x-forms.button>
@ -62,8 +63,11 @@
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="smtpHost" placeholder="smtp.mailgun.org" label="Host" />
<x-forms.input required id="smtpPort" placeholder="587" label="Port" />
<x-forms.input id="smtpEncryption" helper="If SMTP uses SSL, set it to 'tls'."
placeholder="tls" label="Encryption" />
<x-forms.select id="smtpEncryption" label="Encryption">
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
<option value="none">None</option>
</x-forms.select>
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="smtpUsername" label="SMTP Username" />

View file

@ -13,6 +13,8 @@
helper="Allow to automatically deploy Preview Deployments for all opened PR's.<br><br>Closing a PR will delete Preview Deployments."
instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" />
@endif
<x-forms.checkbox helper="Disable Docker build cache on every deployment." instantSave id="disableBuildCache"
label="Disable Build Cache" />
<x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="isForceHttpsEnabled" label="Force Https" />

View file

@ -32,7 +32,7 @@
@forelse ($deployments as $deployment)
<div @class([
'dark:bg-coolgray-100 p-2 border-l-2 transition-colors hover:no-underline box-without-bg-without-border bg-white flex-col cursor-pointer dark:hover:text-neutral-400 dark:hover:bg-coolgray-200',
'border-warning border-dashed ' =>
'border-white border-dashed ' =>
data_get($deployment, 'status') === 'in_progress' ||
data_get($deployment, 'status') === 'cancelled-by-user',
'border-error border-dashed ' =>

View file

@ -19,7 +19,7 @@
},
toggleScroll() {
this.alwaysScroll = !this.alwaysScroll;
if (this.alwaysScroll) {
this.intervalId = setInterval(() => {
const screen = document.getElementById('screen');
@ -58,30 +58,34 @@
<div @if ($isKeepAliveOn) wire:poll.2000ms="polling" @endif
class="flex flex-col-reverse w-full p-2 px-4 mt-4 overflow-y-auto bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300"
:class="fullscreen ? '' : 'min-h-14 max-h-[40rem] border border-dotted rounded'">
<div :class="fullscreen ? 'fixed' : 'absolute'" class="top-4 right-6">
<div :class="fullscreen ? 'fixed' : 'absolute'" class="top-2 right-3">
<div class="flex justify-end gap-4 fixed -translate-x-full">
<button title="Toggle timestamps" x-on:click="showTimestamps = !showTimestamps">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none"
stroke="currentColor" stroke-width="2">
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
<button title="Go Top" x-show="fullscreen" x-on:click="goTop">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-10l-4-4M8 9l4-4" />
</svg>
</button>
<button title="Follow Logs" x-show="fullscreen" :class="alwaysScroll ? 'dark:text-warning' : ''"
x-on:click="toggleScroll">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2" d="M12 5v14m4-4l-4 4m-4-4l4 4" />
</svg>
</button>
<button title="Fullscreen" x-show="!fullscreen" x-on:click="makeFullscreen">
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg class="w-5 h-5 opacity-30 hover:opacity-100" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path
d="M24 0v24H0V0h24ZM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018Zm.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01l-.184-.092Z" />
@ -91,7 +95,8 @@
</svg>
</button>
<button title="Minimize" x-show="fullscreen" x-on:click="makeFullscreen">
<svg class="icon" viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
<svg class="w-5 h-5 opacity-30 hover:opacity-100"
viewBox="0 0 24 24"xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2"
d="M6 14h4m0 0v4m0-4l-6 6m14-10h-4m0 0V6m0 4l6-6" />

View file

@ -15,7 +15,7 @@
volume
name, example: <span class='text-helper'>-pr-1</span>" />
@if ($resource?->build_pack !== 'dockercompose')
<x-modal-input :closeOutside="false" buttonTitle="+ Add" title="New Persistent Storage">
<x-modal-input :closeOutside="false" buttonTitle="+ Add" title="New Persistent Storage" minWidth="64rem">
<livewire:project.shared.storages.add :resource="$resource" />
</x-modal-input>
@endif

View file

@ -1,13 +1,15 @@
<div class="flex flex-col w-full gap-2 rounded max-h-[80vh] overflow-y-auto scrollbar">
<div class="p-4">
You can add Volumes, Files and Directories to your resources here.
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitPersistentVolume'>
<div class="flex flex-col w-full gap-2 max-h-[80vh] overflow-y-auto scrollbar">
<form class="flex flex-col w-full gap-2 rounded " wire:submit='submitPersistentVolume'>
<div class="flex flex-col">
<h3>Volume Mount</h3>
@if ($isSwarm)
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you
would
like to use a persistent volumes.</h5>
@endif
<div>Docker Volumes mounted to the container.</div>
</div>
@if ($isSwarm)
<h5>Swarm Mode detected: You need to set a shared volume (EFS/NFS/etc) on all the worker nodes if you
would
like to use a persistent volumes.</h5>
@endif
<div class="flex flex-col gap-2 px-2">
<x-forms.input placeholder="pv-name" id="name" label="Name" required helper="Volume name." />
@if ($isSwarm)
<x-forms.input placeholder="/root" id="host_path" label="Source Path" required
@ -19,29 +21,39 @@
<x-forms.input placeholder="/tmp/root" id="mount_path" label="Destination Path" required
helper="Directory inside the container." />
<x-forms.button type="submit" @click="modalOpen=false">
Save
Add
</x-forms.button>
</div>
</form>
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorage'>
</form>
<form class="flex flex-col w-full gap-2 rounded py-4" wire:submit='submitFileStorage'>
<div class="flex flex-col">
<h3>File Mount</h3>
<div>Actual file mounted from the host system to the container.</div>
</div>
<div class="flex flex-col gap-2 px-2">
<x-forms.input placeholder="/etc/nginx/nginx.conf" id="file_storage_path" label="Destination Path" required
helper="File inside the container" />
helper="File location inside the container" />
<x-forms.textarea label="Content" id="file_storage_content"></x-forms.textarea>
<x-forms.button type="submit" @click="modalOpen=false">
Save
Add
</x-forms.button>
</form>
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
</div>
</form>
<form class="flex flex-col w-full gap-2 rounded" wire:submit='submitFileStorageDirectory'>
<div class="flex flex-col">
<h3>Directory Mount</h3>
<div>Directory mounted from the host system to the container.</div>
</div>
<div class="flex flex-col gap-2 px-2">
<x-forms.input placeholder="{{ application_configuration_dir() }}/{{ $resource->uuid }}/etc/nginx"
id="file_storage_directory_source" label="Source Directory" required
helper="Directory on the host system." />
<x-forms.input placeholder="/etc/nginx" id="file_storage_directory_destination"
label="Destination Directory" required helper="Directory inside the container." />
<x-forms.button type="submit" @click="modalOpen=false">
Save
Add
</x-forms.button>
</form>
</div>
</div>
</form>
</div>

View file

@ -27,6 +27,13 @@
helper="If set, all resources will only have docker container labels for {{ str($server->proxyType())->title() }}.<br>For applications, labels needs to be regenerated manually. <br>Resources needs to be restarted."
id="server.settings.generate_exact_labels"
label="Generate labels only for {{ str($server->proxyType())->title() }}" instantSave />
<x-forms.checkbox instantSave="instantSaveRedirect" id="redirect_enabled"
label="Override default request handler"
helper="Requests to unknown hosts or stopped services will recieve a 503 response or be redirected to the URL you set below (need to enable this first)." />
@if ($redirect_enabled)
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url"
label="Redirect to (optional)" />
@endif
</div>
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value)
<h4>Traefik</h4>
@ -40,8 +47,6 @@
configurations.
</div>
@endif
<x-forms.input placeholder="https://app.coolify.io" id="redirect_url" label="Default Redirect 404"
helper="All urls that has no service available will be redirected to this domain." />
<div wire:loading wire:target="loadProxyConfiguration" class="pt-4">
<x-loading text="Loading proxy configuration..." />
</div>

View file

@ -29,7 +29,8 @@
@if (str_replace('|', '.', $fileName) === 'coolify.yaml' ||
str_replace('|', '.', $fileName) === 'Caddyfile' ||
str_replace('|', '.', $fileName) === 'coolify.caddy' ||
str_replace('|', '.', $fileName) === 'default_redirect_404.caddy')
str_replace('|', '.', $fileName) === 'default_redirect_503.yaml' ||
str_replace('|', '.', $fileName) === 'default_redirect_503.caddy')
<div>
<h3 class="dark:text-white">File: {{ str_replace('|', '.', $fileName) }}</h3>
</div>

View file

@ -88,7 +88,7 @@
<div class="w-full" x-data="{
open: false,
search: '{{ $serverTimezone ?: '' }}',
timezones: @js($timezones),
timezones: @js($this->timezones),
placeholder: '{{ $serverTimezone ? 'Search timezone...' : 'Select Server Timezone' }}',
init() {
this.$watch('search', value => {

View file

@ -33,8 +33,11 @@
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input required id="smtpHost" placeholder="smtp.mailgun.org" label="Host" />
<x-forms.input required id="smtpPort" placeholder="587" label="Port" />
<x-forms.input id="smtpEncryption" helper="If SMTP uses SSL, set it to 'tls'." placeholder="tls"
label="Encryption" />
<x-forms.select id="smtpEncryption" label="Encryption">
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
<option value="none">None</option>
</x-forms.select>
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="smtpUsername" label="SMTP Username" />

View file

@ -23,7 +23,7 @@
<div class="w-full" x-data="{
open: false,
search: '{{ $settings->instance_timezone ?: '' }}',
timezones: @js($timezones),
timezones: @js($this->timezones),
placeholder: '{{ $settings->instance_timezone ? 'Search timezone...' : 'Select Server Timezone' }}',
init() {
this.$watch('search', value => {

View file

@ -58,7 +58,18 @@
@else
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<x-forms.input id="github_app.name" label="App Name" disabled />
<div class="flex items-end gap-2 w-full">
<x-forms.input id="github_app.name" label="App Name" disabled />
<x-forms.button wire:click.prevent="updateGithubAppName" class="bg-coollabs">
Sync Name
</x-forms.button>
<a href="{{ $this->getGithubAppNameUpdatePath() }}">
<x-forms.button>
Rename
<x-external-link />
</x-forms.button>
</a>
</div>
<x-forms.input id="github_app.organization" label="Organization" disabled
placeholder="If empty, personal user will be used" />
</div>

View file

@ -30,7 +30,7 @@ WARNING_SPACE=false
if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then
WARNING_SPACE=true
cat << 'EOF'
cat << EOF
WARNING: Insufficient total disk space!
Total disk space: ${TOTAL_SPACE}GB
@ -41,7 +41,7 @@ EOF
fi
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then
cat << 'EOF'
cat << EOF
WARNING: Insufficient available disk space!
Available disk space: ${AVAILABLE_SPACE}GB
@ -49,7 +49,7 @@ Required available space: ${REQUIRED_AVAILABLE_SPACE}GB
==================
EOF
WARNING_SPACE=true
WARNING_SPACE=true
fi
if [ "$WARNING_SPACE" = true ]; then

View file

@ -1,6 +1,7 @@
# documentation: https://github.com/linuxserver/Heimdall
# slogan: Heimdall is a dashboard for managing and organizing your server applications.
# tags: dashboard, server, applications, interface
# logo: svgs/heimdall.svg
services:
heimdall:

View file

@ -1,6 +1,7 @@
# documentation: https://invoiceninja.github.io/selfhost.html
# slogan: The leading open-source invoicing platform
# tags: invoicing, billing, accounting, finance, self-hosted
# logo: svgs/invoiceninja.png
# port: 9000
services:

View file

@ -1,6 +1,7 @@
# documentation: https://kuzzle.io
# slogan: Kuzzle is a generic backend offering the basic building blocks common to every application.
# tags: backend, api, realtime, websocket, mqtt, rest, sdk, iot, geofencing, low-code
# logo: svgs/kuzzle.png
# port: 7512
services:

View file

@ -1,6 +1,7 @@
# documentation: https://pairdrop.net/
# slogan: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.
# tags: file, sharing, collaboration, teamwork
# logo: svgs/pairdrop.png
# port: 3000
services:

View file

@ -1,6 +1,7 @@
# documentation: https://help.penpot.app/technical-guide/getting-started/#install-with-docker
# slogan: Penpot is the first Open Source design and prototyping platform for product teams.
# tags: penpot,design,prototyping,figma,open,source
# logo: svgs/penpot.svg
services:
frontend:

View file

@ -35,6 +35,7 @@ services:
- RESEND_API_KEY=${RESEND_API_KEY}
- EMAIL_FROM_ADDRESS=${EMAIL_FROM_ADDRESS}
- EMAIL_FROM_NAME=${EMAIL_FROM_NAME}
- EMAIL_PROVIDER=${EMAIL_PROVIDER}
# Social Media API Settings
- X_API_KEY=${SERVICE_X_API}

View file

@ -5,7 +5,6 @@
# port: 3000
x-common-env: &common-env
PORT: 3030
REMIX_APP_PORT: 3000
NODE_ENV: production
RUNTIME_PLATFORM: docker-compose
@ -118,7 +117,7 @@ services:
environment:
<<: *common-env
PLATFORM_HOST: trigger
PLATFORM_WS_PORT: 3030
PLATFORM_WS_PORT: 3000
SECURE_CONNECTION: "false"
PLATFORM_SECRET: $PROVIDER_SECRET
coordinator:
@ -133,7 +132,7 @@ services:
environment:
<<: *common-env
PLATFORM_HOST: trigger
PLATFORM_WS_PORT: 3030
PLATFORM_WS_PORT: 3000
SECURE_CONNECTION: "false"
PLATFORM_SECRET: $COORDINATOR_SECRET
healthcheck:

View file

@ -1,6 +1,7 @@
# documentation: https://github.com/benbusby/whoogle-search?tab=readme-ov-file
# slogan: Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.
# tags: privacy, search engine
# logo: svgs/whoogle.png
# port: 5000
services:

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,57 @@
<?php
namespace Tests\Feature;
use App\Models\Application;
use App\Models\Server;
use App\Models\User;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
use Tests\Traits\HandlesTestDatabase;
class ExecuteContainerCommandTest extends TestCase
{
use HandlesTestDatabase;
private $user;
private $team;
private $server;
private $application;
protected function setUp(): void
{
parent::setUp();
// Only set up database for tests that need it
if ($this->shouldSetUpDatabase()) {
$this->setUpTestDatabase();
}
// Create test data
$this->user = User::factory()->create();
$this->team = $this->user->teams()->first();
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->application = Application::factory()->create();
// Login the user
$this->actingAs($this->user);
}
protected function tearDown(): void
{
if ($this->shouldSetUpDatabase()) {
$this->tearDownTestDatabase();
}
parent::tearDown();
}
private function shouldSetUpDatabase(): bool
{
return in_array($this->name(), [
'it_allows_valid_container_access',
'it_prevents_cross_server_container_access',
]);
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Tests\Traits;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
trait HandlesTestDatabase
{
protected function setUpTestDatabase(): void
{
try {
// Create test database if it doesn't exist
$database = config('database.connections.testing.database');
$this->createTestDatabase($database);
// Run migrations
Artisan::call('migrate:fresh', [
'--database' => 'testing',
'--seed' => false,
]);
} catch (\Exception $e) {
$this->tearDownTestDatabase();
throw $e;
}
}
protected function tearDownTestDatabase(): void
{
try {
// Drop test database
$database = config('database.connections.testing.database');
$this->dropTestDatabase($database);
} catch (\Exception $e) {
// Log error but don't throw
error_log('Failed to tear down test database: '.$e->getMessage());
}
}
protected function createTestDatabase($database)
{
try {
// Connect to postgres database to create/drop test database
config(['database.connections.pgsql.database' => 'postgres']);
DB::purge('pgsql');
DB::reconnect('pgsql');
// Drop if exists and create new database
DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
DB::connection('pgsql')->statement("CREATE DATABASE $database;");
// Switch back to testing connection
DB::disconnect('pgsql');
DB::reconnect('testing');
} catch (\Exception $e) {
$this->tearDownTestDatabase();
throw new \Exception('Could not create test database: '.$e->getMessage());
}
}
protected function dropTestDatabase($database)
{
try {
// Connect to postgres database to drop test database
config(['database.connections.pgsql.database' => 'postgres']);
DB::purge('pgsql');
DB::reconnect('pgsql');
// Drop the test database
DB::connection('pgsql')->statement("DROP DATABASE IF EXISTS $database WITH (FORCE);");
DB::disconnect('pgsql');
} catch (\Exception $e) {
// Log error but don't throw
error_log('Failed to drop test database: '.$e->getMessage());
}
}
}

View file

@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
"version": "4.0.0-beta.375"
"version": "4.0.0-beta.376"
},
"nightly": {
"version": "4.0.0-beta.376"
"version": "4.0.0-beta.377"
},
"helper": {
"version": "1.0.4"
@ -16,4 +16,4 @@
"version": "0.0.15"
}
}
}
}