mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
Merge branch 'next' into docker-image-login
This commit is contained in:
commit
d529f8f8aa
62 changed files with 682 additions and 189 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'])]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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'])]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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' : '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|
|
|
|||
22
database/factories/ApplicationFactory.php
Normal file
22
database/factories/ApplicationFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
17
database/factories/ServerFactory.php
Normal file
17
database/factories/ServerFactory.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
10
openapi.json
10
openapi.json
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
11
public/svgs/heimdall.svg
Normal 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 |
BIN
public/svgs/invoiceninja.png
Normal file
BIN
public/svgs/invoiceninja.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
public/svgs/kuzzle.png
Normal file
BIN
public/svgs/kuzzle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/svgs/pairdrop.png
Normal file
BIN
public/svgs/pairdrop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
3
public/svgs/penpot.svg
Normal file
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
BIN
public/svgs/whoogle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 ' =>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
57
tests/Feature/ExecuteContainerCommandTest.php
Normal file
57
tests/Feature/ExecuteContainerCommandTest.php
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
78
tests/Traits/HandlesTestDatabase.php
Normal file
78
tests/Traits/HandlesTestDatabase.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue