Merge branch 'coollabsio:next' into next

This commit is contained in:
Nathan James
2025-07-04 16:07:31 +01:00
committed by GitHub
670 changed files with 37135 additions and 10169 deletions

View File

@@ -17,11 +17,22 @@ class Configuration extends Component
public $servers;
protected $listeners = ['buildPackUpdated' => '$refresh'];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
"echo-private:team.{$teamId},ServiceStatusChanged" => '$refresh',
'buildPackUpdated' => '$refresh',
'refresh' => '$refresh',
];
}
public function mount()
{
$this->currentRoute = request()->route()->getName();
$project = currentTeam()
->projects()
->select('id', 'uuid', 'team_id')
@@ -39,6 +50,14 @@ class Configuration extends Component
$this->project = $project;
$this->environment = $environment;
$this->application = $application;
if ($this->application->deploymentType() === 'deploy_key' && $this->currentRoute === 'project.application.preview-deployments') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') {
return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]);
}
}
public function render()

View File

@@ -28,6 +28,15 @@ class Index extends Component
protected $queryString = ['pull_request_id'];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
];
}
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@@ -18,7 +18,15 @@ class Show extends Component
public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue'];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
'refreshQueue',
];
}
public function mount()
{

View File

@@ -53,13 +53,13 @@ class DeploymentNavbar extends Component
public function cancel()
{
$kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
$build_server_id = $this->application_deployment_queue->build_server_id;
$build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id;
$server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
try {
if ($this->application->settings->is_build_server_enabled) {
$server = Server::find($build_server_id);
$server = Server::ownedByCurrentTeam()->find($build_server_id);
} else {
$server = Server::find($server_id);
$server = Server::ownedByCurrentTeam()->find($server_id);
}
if ($this->application_deployment_queue->logs) {
$previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);

View File

@@ -68,6 +68,7 @@ class General extends Component
'application.publish_directory' => 'nullable',
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.custom_network_aliases' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
@@ -86,10 +87,14 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable',
'application.custom_nginx_configuration' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_spa' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.settings.is_container_label_readonly_enabled' => 'boolean|required',
'application.settings.is_preserve_repository_enabled' => 'boolean|required',
'application.is_http_basic_auth_enabled' => 'boolean|required',
'application.http_basic_auth_username' => 'string|nullable',
'application.http_basic_auth_password' => 'string|nullable',
'application.watch_paths' => 'nullable',
'application.redirect' => 'string|required',
];
@@ -120,10 +125,12 @@ class General extends Component
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
'application.custom_docker_run_options' => 'Custom docker run commands',
'application.custom_network_aliases' => 'Custom docker network aliases',
'application.docker_compose_custom_start_command' => 'Docker compose custom start command',
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.custom_nginx_configuration' => 'Custom Nginx configuration',
'application.settings.is_static' => 'Is static',
'application.settings.is_spa' => 'Is SPA',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.settings.is_container_label_readonly_enabled' => 'Is container label readonly',
@@ -171,6 +178,12 @@ class General extends Component
public function instantSave()
{
if ($this->application->settings->isDirty('is_spa')) {
$this->generateNginxConfiguration($this->application->settings->is_spa ? 'spa' : 'static');
}
if ($this->application->isDirty('is_http_basic_auth_enabled')) {
$this->application->save();
}
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
@@ -190,6 +203,7 @@ class General extends Component
if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
}
public function loadComposeFile($isInit = false)
@@ -287,9 +301,9 @@ class General extends Component
}
}
public function generateNginxConfiguration()
public function generateNginxConfiguration($type = 'static')
{
$this->application->custom_nginx_configuration = defaultNginxConfiguration();
$this->application->custom_nginx_configuration = defaultNginxConfiguration($type);
$this->application->save();
$this->dispatch('success', 'Nginx configuration generated.');
}
@@ -369,6 +383,9 @@ class General extends Component
if ($this->application->isDirty('redirect')) {
$this->setRedirect();
}
if ($this->application->isDirty('dockerfile')) {
$this->application->parseHealthcheckFromDockerfile($this->application->dockerfile);
}
$this->checkFqdns();
@@ -446,7 +463,6 @@ class General extends Component
{
$config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json');
dd($config);
return response()->streamDownload(function () use ($config) {
echo $config;

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
use App\Models\Application;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -28,7 +27,8 @@ class Heading extends Component
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'check_status',
"echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus',
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
'compose_loaded' => '$refresh',
'update_links' => '$refresh',
];
@@ -46,13 +46,12 @@ class Heading extends Component
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
}
public function check_status($showNotification = false)
public function checkStatus()
{
if ($this->application->destination->server->isFunctional()) {
GetContainersStatus::dispatch($this->application->destination->server);
}
if ($showNotification) {
$this->dispatch('success', 'Success', 'Application status updated.');
} else {
$this->dispatch('error', 'Server is not functional.');
}
}
@@ -84,18 +83,23 @@ class Heading extends Component
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
force_rebuild: $force_rebuild,
);
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: true);
], navigate: false);
}
protected function setDeploymentUuid()
@@ -106,16 +110,8 @@ class Heading extends Component
public function stop()
{
StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
$this->application->additional_servers->each(function ($server) {
$server->pivot->status = 'exited:unhealthy';
$server->pivot->save();
});
}
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
$this->dispatch('info', 'Gracefully stopping application.<br/>It could take a while depending on the application.');
StopApplication::dispatch($this->application, false, $this->docker_cleanup);
}
public function restart()
@@ -126,18 +122,23 @@ class Heading extends Component
return;
}
$this->setDeploymentUuid();
queue_application_deployment(
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deploymentUuid,
restart_only: true,
);
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid,
'environment_uuid' => $this->parameters['environment_uuid'],
], navigate: true);
], navigate: false);
}
public function render()

View File

@@ -5,10 +5,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -141,13 +138,18 @@ class Previews extends Component
}
}
public function force_deploy_without_cache(int $pull_request_id, ?string $pull_request_html_url = null)
{
$this->deploy($pull_request_id, $pull_request_html_url, force_rebuild: true);
}
public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
{
$this->add($pull_request_id, $pull_request_html_url);
$this->deploy($pull_request_id, $pull_request_html_url);
}
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null)
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null, bool $force_rebuild = false)
{
try {
$this->setDeploymentUuid();
@@ -159,13 +161,18 @@ class Previews extends Component
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
$result = queue_application_deployment(
application: $this->application,
deployment_uuid: $this->deployment_uuid,
force_rebuild: false,
force_rebuild: $force_rebuild,
pull_request_id: $pull_request_id,
git_type: $found->git_type ?? null,
);
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return redirect()->route('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'],
@@ -188,13 +195,12 @@ class Previews extends Component
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
$this->stopContainers($containers, $server);
}
GetContainersStatus::run($server);
@@ -210,13 +216,12 @@ class Previews extends Component
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
$this->stopContainers($containers, $server);
}
ApplicationPreview::where('application_id', $this->application->id)
@@ -232,48 +237,26 @@ class Previews extends Component
}
}
private function stopContainers(array $containers, $server, int $timeout)
private function stopContainers(array $containers, $server, int $timeout = 30)
{
$processes = [];
if (empty($containers)) {
return;
}
$containerNames = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
$containerNames[] = str_replace('/', '', $container['Names']);
}
$startTime = Carbon::now()->getTimestamp();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
$containerList = implode(' ', array_map('escapeshellarg', $containerNames));
$commands = [
"docker stop --time=$timeout $containerList",
"docker rm -f $containerList",
];
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
}
instant_remote_process(
command: $commands,
server: $server,
throwError: false
);
}
}

View File

@@ -30,11 +30,15 @@ class Source extends Component
#[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null;
#[Locked]
public $sources;
public function mount()
{
try {
$this->syncData();
$this->getPrivateKeys();
$this->getSources();
} catch (\Throwable $e) {
handleError($e, $this);
}
@@ -66,6 +70,14 @@ class Source extends Component
});
}
private function getSources()
{
// filter the current source out
$this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) {
return $source->id === $this->application->source_id;
})->sortBy('name');
}
public function setPrivateKey(int $privateKeyId)
{
try {
@@ -92,4 +104,31 @@ class Source extends Component
return handleError($e, $this);
}
}
public function changeSource($sourceId, $sourceType)
{
try {
$this->application->update([
'source_id' => $sourceId,
'source_type' => $sourceType,
]);
['repository' => $customRepository] = $this->application->customRepository();
$repository = githubApi($this->application->source, "repos/{$customRepository}");
$data = data_get($repository, 'data');
$repository_project_id = data_get($data, 'id');
if (isset($repository_project_id)) {
if ($this->application->repository_project_id !== $repository_project_id) {
$this->application->repository_project_id = $repository_project_id;
$this->application->save();
}
}
$this->application->refresh();
$this->getSources();
$this->dispatch('success', 'Source updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -54,7 +54,10 @@ class CloneMe extends Component
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->servers = currentTeam()
->servers()
->get()
->reject(fn ($server) => $server->isBuildServer());
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Database;
use Auth;
use Livewire\Component;
class Configuration extends Component
@@ -14,6 +15,15 @@ class Configuration extends Component
public $environment;
public function getListeners()
{
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
];
}
public function mount()
{
$this->currentRoute = request()->route()->getName();

View File

@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Dragonfly;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneDragonfly;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@@ -50,12 +53,19 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['nullable', 'boolean'])]
public bool $enable_ssl = false;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
];
}
@@ -64,6 +74,12 @@ class General extends Component
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -82,6 +98,7 @@ class General extends Component
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@@ -96,6 +113,7 @@ class General extends Component
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@@ -174,4 +192,61 @@ class General extends Component
}
}
}
public function instantSaveSSL()
{
try {
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$server = $this->database->destination->server;
$caCert = SslCertificate::where('server_id', $server->id)
->where('is_ca_certificate', true)
->first();
if (! $caCert) {
$server->generateCaCertificate();
$caCert = SslCertificate::where('server_id', $server->id)->where('is_ca_certificate', true)->first();
}
if (! $caCert) {
$this->dispatch('error', 'No CA certificate found for this database. Please generate a CA certificate for this server in the server/advanced page.');
return;
}
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
}

View File

@@ -6,7 +6,7 @@ use App\Actions\Database\RestartDatabase;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
use Illuminate\Support\Facades\Auth;
use App\Events\ServiceStatusChanged;
use Livewire\Component;
class Heading extends Component
@@ -19,34 +19,40 @@ class Heading extends Component
public function getListeners()
{
$userId = Auth::id();
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished',
"echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'activityFinished',
'refresh' => '$refresh',
'compose_loaded' => '$refresh',
'update_links' => '$refresh',
];
}
public function activityFinished()
{
$this->database->update([
'started_at' => now(),
]);
$this->dispatch('refresh');
$this->check_status();
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
$this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
} else {
try {
$this->database->started_at ??= now();
$this->database->save();
if (is_null($this->database->config_hash) || $this->database->isConfigurationChanged()) {
$this->database->isConfigurationChanged(true);
}
$this->dispatch('configurationChanged');
} catch (\Exception $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh');
}
}
public function check_status($showNotification = false)
public function checkStatus()
{
GetContainersStatus::run($this->database->destination->server);
$this->database->refresh();
if ($showNotification) {
$this->dispatch('success', 'Database status updated.');
if ($this->database->destination->server->isFunctional()) {
GetContainersStatus::dispatch($this->database->destination->server);
} else {
$this->dispatch('error', 'Server is not functional.');
}
}
@@ -57,22 +63,24 @@ class Heading extends Component
public function stop()
{
StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
try {
$this->dispatch('info', 'Gracefully stopping database.');
StopDatabase::dispatch($this->database, false, $this->docker_cleanup);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function restart()
{
$activity = RestartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
}
public function start()
{
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
}
public function render()

View File

@@ -4,8 +4,11 @@ namespace App\Livewire\Project\Database\Keydb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneKeydb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Validate;
@@ -53,12 +56,20 @@ class General extends Component
#[Validate(['nullable', 'boolean'])]
public bool $isLogDrainEnabled = false;
public ?Carbon $certificateValidUntil = null;
#[Validate(['boolean'])]
public bool $enable_ssl = false;
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
@@ -67,6 +78,12 @@ class General extends Component
try {
$this->syncData();
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -86,6 +103,7 @@ class General extends Component
$this->database->public_port = $this->publicPort;
$this->database->custom_docker_run_options = $this->customDockerRunOptions;
$this->database->is_log_drain_enabled = $this->isLogDrainEnabled;
$this->database->enable_ssl = $this->enable_ssl;
$this->database->save();
$this->dbUrl = $this->database->internal_db_url;
@@ -101,6 +119,7 @@ class General extends Component
$this->publicPort = $this->database->public_port;
$this->customDockerRunOptions = $this->database->custom_docker_run_options;
$this->isLogDrainEnabled = $this->database->is_log_drain_enabled;
$this->enable_ssl = $this->database->enable_ssl;
$this->dbUrl = $this->database->internal_db_url;
$this->dbUrlPublic = $this->database->external_db_url;
}
@@ -179,4 +198,48 @@ class General extends Component
}
}
}
public function instantSaveSSL()
{
try {
$this->syncData(true);
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)
->where('is_ca_certificate', true)
->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
}

View File

@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mariadb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMariadb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -35,6 +51,7 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
];
protected $validationAttributes = [
@@ -50,6 +67,7 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Options',
'database.enable_ssl' => 'Enable SSL',
];
public function mount()
@@ -57,6 +75,12 @@ class General extends Component
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@@ -127,6 +151,48 @@ class General extends Component
}
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View File

@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mongodb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMongodb;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -34,6 +50,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-full',
];
protected $validationAttributes = [
@@ -48,6 +66,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -55,6 +75,12 @@ class General extends Component
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@@ -128,6 +154,53 @@ class General extends Component
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View File

@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Mysql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneMysql;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@@ -21,6 +25,18 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -35,6 +51,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:PREFERRED,REQUIRED,VERIFY_CA,VERIFY_IDENTITY',
];
protected $validationAttributes = [
@@ -50,6 +68,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -57,6 +77,12 @@ class General extends Component
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@@ -127,6 +153,53 @@ class General extends Component
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View File

@@ -4,9 +4,13 @@ namespace App\Livewire\Project\Database\Postgresql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandalonePostgresql;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
@@ -23,10 +27,15 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
'refresh',
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'refresh' => '$refresh',
'save_init_script',
'delete_init_script',
];
@@ -48,6 +57,8 @@ class General extends Component
'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
'database.custom_docker_run_options' => 'nullable',
'database.enable_ssl' => 'boolean',
'database.ssl_mode' => 'nullable|string|in:allow,prefer,require,verify-ca,verify-full',
];
protected $validationAttributes = [
@@ -65,6 +76,8 @@ class General extends Component
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
'database.custom_docker_run_options' => 'Custom Docker Run Options',
'database.enable_ssl' => 'Enable SSL',
'database.ssl_mode' => 'SSL Mode',
];
public function mount()
@@ -72,6 +85,12 @@ class General extends Component
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
$this->server = data_get($this->database, 'destination.server');
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@@ -91,6 +110,55 @@ class General extends Component
}
}
public function updatedDatabaseSslMode()
{
$this->instantSaveSSL();
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->common_name,
subjectAlternativeNames: $existingCert->subject_alternative_names ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates have been regenerated. Please restart the database for changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
@@ -143,7 +211,7 @@ class General extends Component
$delete_command = "rm -f $old_file_path";
try {
instant_remote_process([$delete_command], $this->server);
} catch (\Exception $e) {
} catch (Exception $e) {
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
return;
@@ -184,7 +252,7 @@ class General extends Component
$command = "rm -f $file_path";
try {
instant_remote_process([$command], $this->server);
} catch (\Exception $e) {
} catch (Exception $e) {
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
return;
@@ -201,16 +269,11 @@ class General extends Component
$this->database->init_scripts = $updatedScripts;
$this->database->save();
$this->refresh();
$this->dispatch('refresh')->self();
$this->dispatch('success', 'Init script deleted from the database and the server.');
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function save_new_init_script()
{
$this->validate([

View File

@@ -4,25 +4,24 @@ namespace App\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Helpers\SslHelper;
use App\Models\Server;
use App\Models\SslCertificate;
use App\Models\StandaloneRedis;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class General extends Component
{
protected $listeners = [
'envsUpdated' => 'refresh',
'refresh',
];
public Server $server;
public StandaloneRedis $database;
public string $redis_username;
public string $redis_password;
public ?string $redis_password;
public string $redis_version;
@@ -30,6 +29,19 @@ class General extends Component
public ?string $db_url_public = null;
public ?Carbon $certificateValidUntil = null;
public function getListeners()
{
$userId = Auth::id();
return [
"echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh',
'envsUpdated' => 'refresh',
'refresh',
];
}
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
@@ -42,6 +54,7 @@ class General extends Component
'database.custom_docker_run_options' => 'nullable',
'redis_username' => 'required',
'redis_password' => 'required',
'database.enable_ssl' => 'boolean',
];
protected $validationAttributes = [
@@ -55,12 +68,18 @@ class General extends Component
'database.custom_docker_run_options' => 'Custom Docker Options',
'redis_username' => 'Redis Username',
'redis_password' => 'Redis Password',
'database.enable_ssl' => 'Enable SSL',
];
public function mount()
{
$this->server = data_get($this->database, 'destination.server');
$this->refreshView();
$existingCert = $this->database->sslCertificates()->first();
if ($existingCert) {
$this->certificateValidUntil = $existingCert->valid_until;
}
}
public function instantSaveAdvanced()
@@ -136,6 +155,48 @@ class General extends Component
}
}
public function instantSaveSSL()
{
try {
$this->database->save();
$this->dispatch('success', 'SSL configuration updated.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function regenerateSslCertificate()
{
try {
$existingCert = $this->database->sslCertificates()->first();
if (! $existingCert) {
$this->dispatch('error', 'No existing SSL certificate found for this database.');
return;
}
$caCert = SslCertificate::where('server_id', $existingCert->server_id)->where('is_ca_certificate', true)->first();
SslHelper::generateSslCertificate(
commonName: $existingCert->commonName,
subjectAlternativeNames: $existingCert->subjectAlternativeNames ?? [],
resourceType: $existingCert->resource_type,
resourceId: $existingCert->resource_id,
serverId: $existingCert->server_id,
caCert: $caCert->ssl_certificate,
caKey: $caCert->ssl_private_key,
configurationDir: $existingCert->configuration_dir,
mountPath: $existingCert->mount_path,
isPemKeyFileRequired: true,
);
$this->dispatch('success', 'SSL certificates regenerated. Restart database to apply changes.');
} catch (Exception $e) {
handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();

View File

@@ -19,6 +19,8 @@ class ScheduledBackups extends Component
public $s3s;
public string $custom_type = 'mysql';
protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId'];
@@ -49,6 +51,14 @@ class ScheduledBackups extends Component
}
}
public function setCustomType()
{
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
}
public function delete($scheduled_backup_id): void
{
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@@ -62,5 +72,6 @@ class ScheduledBackups extends Component
if ($id) {
$this->setSelectedBackup($id);
}
$this->dispatch('refreshScheduledBackups');
}
}

View File

@@ -35,6 +35,6 @@ class Index extends Component
{
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
return $this->redirect($project->navigateTo(), true);
return $this->redirect($project->navigateTo(), navigate: false);
}
}

View File

@@ -7,7 +7,6 @@ use App\Models\Project;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Illuminate\Support\Str;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
@@ -26,21 +25,7 @@ class DockerCompose extends Component
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (isDev()) {
$this->dockerComposeRaw = 'services:
appsmith:
build:
context: .
dockerfile_inline: |
FROM nginx
ARG GIT_COMMIT
ARG GIT_BRANCH
RUN echo "Hello World ${GIT_COMMIT} ${GIT_BRANCH}"
args:
- GIT_COMMIT=cdc3b19
- GIT_BRANCH=${GIT_BRANCH}
environment:
- APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED}
';
$this->dockerComposeRaw = file_get_contents(base_path('templates/test-database-detection.yaml'));
}
}
@@ -52,12 +37,6 @@ class DockerCompose extends Component
'dockerComposeRaw' => 'required',
]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$isValid = validateComposeFile($this->dockerComposeRaw, $server_id);
if ($isValid !== 'OK') {
return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
}
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
@@ -72,7 +51,6 @@ class DockerCompose extends Component
$destination_class = $destination->getMorphClass();
$service = Service::create([
'name' => 'service'.Str::random(10),
'docker_compose_raw' => $this->dockerComposeRaw,
'environment_id' => $environment->id,
'server_id' => (int) $server_id,
@@ -91,8 +69,6 @@ class DockerCompose extends Component
'resourceable_type' => $service->getMorphClass(),
]);
}
$service->name = "service-$service->uuid";
$service->parse(isNew: true);
return redirect()->route('project.service.configuration', [

View File

@@ -106,11 +106,15 @@ class GithubPrivateRepository extends Component
$this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first();
$this->token = generateGithubInstallationToken($this->github_app);
$this->loadRepositoryByPage();
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
$this->total_repositories_count = $repositories['total_count'];
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) {
$this->page++;
$this->loadRepositoryByPage();
$repositories = loadRepositoryByPage($this->github_app, $this->token, $this->page);
$this->total_repositories_count = $repositories['total_count'];
$this->repositories = $this->repositories->concat(collect($repositories['repositories']));
}
}
$this->repositories = $this->repositories->sortBy('name');
@@ -120,21 +124,6 @@ class GithubPrivateRepository extends Component
$this->current_step = 'repository';
}
protected function loadRepositoryByPage()
{
$response = Http::withToken($this->token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100&page={$this->page}");
$json = $response->json();
if ($response->status() !== 200) {
return $this->dispatch('error', $json['message']);
}
if ($json['total_count'] === 0) {
return;
}
$this->total_repositories_count = $json['total_count'];
$this->repositories = $this->repositories->concat(collect($json['repositories']));
}
public function loadBranches()
{
$this->selected_repository_owner = $this->repositories->where('id', $this->selected_repository_id)->first()['owner']['login'];

View File

@@ -57,13 +57,18 @@ class Select extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
try {
$this->parameters = get_route_parameters();
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$project = Project::whereUuid($projectUuid)->firstOrFail();
$this->environments = $project->environments;
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
} catch (\Exception $e) {
return handleError($e, $this);
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_uuid');
}
public function render()
@@ -73,15 +78,17 @@ class Select extends Component
public function updatedSelectedEnvironment()
{
$environmentUuid = $this->environments->where('name', $this->selectedEnvironment)->first()->uuid;
return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_uuid' => $this->selectedEnvironment,
'environment_uuid' => $environmentUuid,
]);
}
public function loadServices()
{
$services = get_service_templates(true);
$services = get_service_templates();
$services = collect($services)->map(function ($service, $key) {
$default_logo = 'images/default.webp';
$logo = data_get($service, 'logo', $default_logo);

View File

@@ -74,7 +74,7 @@ CMD ["nginx", "-g", "daemon off;"]
'fqdn' => $fqdn,
]);
$application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true);
$application->parseHealthcheckFromDockerfile(dockerfile: $this->dockerfile, isInit: true);
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,

View File

@@ -73,7 +73,6 @@ class Create extends Component
if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service_payload = [
'name' => "$oneClickServiceName-".str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,

View File

@@ -2,7 +2,6 @@
namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Service;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
@@ -27,12 +26,10 @@ class Configuration extends Component
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
'refresh' => '$refresh',
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
];
}
@@ -63,6 +60,13 @@ class Configuration extends Component
$this->databases = $this->service->databases->sort();
}
public function refreshServices()
{
$this->service->refresh();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function restartApplication($id)
{
try {
@@ -89,17 +93,15 @@ class Configuration extends Component
}
}
public function check_status()
public function serviceChecked()
{
try {
GetContainersStatus::run($this->service->server);
$this->service->applications->each(function ($application) {
$application->refresh();
});
$this->service->databases->each(function ($database) {
$database->refresh();
});
$this->dispatch('refresh');
} catch (\Exception $e) {
return handleError($e, $this);
}

View File

@@ -4,7 +4,11 @@ namespace App\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\InstanceSettings;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Database extends Component
@@ -15,6 +19,8 @@ class Database extends Component
public $fileStorages;
public $parameters;
protected $listeners = ['refreshFileStorages'];
protected $rules = [
@@ -34,12 +40,33 @@ class Database extends Component
public function mount()
{
$this->parameters = get_route_parameters();
if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl();
}
$this->refreshFileStorages();
}
public function delete($password)
{
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
}
try {
$this->database->delete();
$this->dispatch('success', 'Database deleted.');
return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSaveExclude()
{
$this->submit();
@@ -57,6 +84,42 @@ class Database extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
public function convertToApplication()
{
try {
$service = $this->database->service;
$serviceDatabase = $this->database;
// Check if application with same name already exists
if ($service->applications()->where('name', $serviceDatabase->name)->exists()) {
throw new \Exception('An application with this name already exists.');
}
// Create new parameters removing database_uuid
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceDatabase) {
$service->applications()->create([
'name' => $serviceDatabase->name,
'human_name' => $serviceDatabase->human_name,
'description' => $serviceDatabase->description,
'exclude_from_status' => $serviceDatabase->exclude_from_status,
'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled,
'image' => $serviceDatabase->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceDatabase->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
if ($this->database->is_public && ! $this->database->public_port) {

View File

@@ -31,12 +31,22 @@ class EditCompose extends Component
public function refreshEnvs()
{
$this->service = Service::find($this->serviceId);
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
}
public function mount()
{
$this->service = Service::find($this->serviceId);
$this->service = Service::ownedByCurrentTeam()->find($this->serviceId);
}
public function validateCompose()
{
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server_id);
if ($isValid !== 'OK') {
$this->dispatch('error', "Invalid docker-compose file.\n$isValid");
} else {
$this->dispatch('success', 'Docker compose is valid.');
}
}
public function saveEditedCompose()

View File

@@ -43,8 +43,6 @@ class EditDomain extends Component
updateCompose($this->application);
if (str($this->application->fqdn)->contains(',')) {
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
} else {
! $warning && $this->dispatch('success', 'Service saved.');
}
$this->application->service->parse();
$this->dispatch('refresh');

View File

@@ -49,7 +49,6 @@ class FileStorage extends Component
$this->workdir = null;
$this->fs_path = $this->fileStorage->fs_path;
}
$this->fileStorage->loadStorageOnServer();
}
public function convertToDirectory()
@@ -68,6 +67,18 @@ class FileStorage extends Component
}
}
public function loadStorageOnServer()
{
try {
$this->fileStorage->loadStorageOnServer();
$this->dispatch('success', 'File storage loaded from server.');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refreshStorages');
}
}
public function convertToFile()
{
try {

View File

@@ -2,17 +2,16 @@
namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Actions\Shared\PullImage;
use App\Enums\ProcessStatus;
use App\Events\ServiceStatusChanged;
use App\Models\Service;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use Spatie\Activitylog\Models\Activity;
class Navbar extends Component
class Heading extends Component
{
public Service $service;
@@ -36,34 +35,44 @@ class Navbar extends Component
public function getListeners()
{
$userId = Auth::id();
$teamId = Auth::user()->currentTeam()->id;
return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
"echo-private:team.{$teamId},ServiceStatusChanged" => 'checkStatus',
"echo-private:team.{$teamId},ServiceChecked" => 'serviceChecked',
'refresh' => '$refresh',
'envsUpdated' => '$refresh',
];
}
public function serviceStarted()
public function checkStatus()
{
// $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
if ($this->service->server->isFunctional()) {
GetContainersStatus::dispatch($this->service->server);
} else {
$this->dispatch('configurationChanged');
$this->dispatch('error', 'Server is not functional.');
}
}
public function check_status_without_notification()
public function serviceChecked()
{
$this->dispatch('check_status');
}
try {
$this->service->applications->each(function ($application) {
$application->refresh();
});
$this->service->databases->each(function ($database) {
$database->refresh();
});
if (is_null($this->service->config_hash)) {
$this->service->isConfigurationChanged(true);
}
$this->dispatch('configurationChanged');
} catch (\Exception $e) {
return handleError($e, $this);
} finally {
$this->dispatch('refresh')->self();
}
public function check_status()
{
$this->dispatch('check_status');
$this->dispatch('success', 'Service status updated.');
}
public function checkDeployments()
@@ -85,37 +94,33 @@ class Navbar extends Component
public function start()
{
$this->service->parse();
$activity = StartService::run($this->service);
$activity = StartService::run($this->service, pullLatestImages: true);
$this->dispatch('activityMonitor', $activity->id);
}
public function forceDeploy()
{
try {
$activities = Activity::where('properties->type_uuid', $this->service->uuid)->where('properties->status', ProcessStatus::IN_PROGRESS->value)->orWhere('properties->status', ProcessStatus::QUEUED->value)->get();
$activities = Activity::where('properties->type_uuid', $this->service->uuid)
->where(function ($q) {
$q->where('properties->status', ProcessStatus::IN_PROGRESS->value)
->orWhere('properties->status', ProcessStatus::QUEUED->value);
})->get();
foreach ($activities as $activity) {
$activity->properties->status = ProcessStatus::ERROR->value;
$activity->save();
}
$this->service->parse();
$activity = StartService::run($this->service);
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
}
public function stop($cleanupContainers = false)
public function stop()
{
try {
StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
if ($cleanupContainers) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
StopService::dispatch($this->service, false, $this->docker_cleanup);
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
}
@@ -129,10 +134,7 @@ class Navbar extends Component
return;
}
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$activity = StartService::run($this->service, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
}
@@ -144,17 +146,13 @@ class Navbar extends Component
return;
}
PullImage::run($this->service);
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
$this->dispatch('activityMonitor', $activity->id);
}
public function render()
{
return view('livewire.project.service.navbar', [
return view('livewire.project.service.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],

View File

@@ -24,7 +24,7 @@ class Index extends Component
public $s3s;
protected $listeners = ['generateDockerCompose'];
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh'];
public function mount()
{

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\InstanceSettings;
use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@@ -23,7 +24,7 @@ class ServiceApplicationView extends Component
'application.human_name' => 'nullable',
'application.description' => 'nullable',
'application.fqdn' => 'nullable',
'application.image' => 'required',
'application.image' => 'string|nullable',
'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
@@ -73,6 +74,40 @@ class ServiceApplicationView extends Component
$this->parameters = get_route_parameters();
}
public function convertToDatabase()
{
try {
$service = $this->application->service;
$serviceApplication = $this->application;
// Check if database with same name already exists
if ($service->databases()->where('name', $serviceApplication->name)->exists()) {
throw new \Exception('A database with this name already exists.');
}
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceApplication) {
$service->databases()->create([
'name' => $serviceApplication->name,
'human_name' => $serviceApplication->human_name,
'description' => $serviceApplication->description,
'exclude_from_status' => $serviceApplication->exclude_from_status,
'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled,
'image' => $serviceApplication->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceApplication->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit()
{
try {

View File

@@ -63,7 +63,7 @@ class StackForm extends Component
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit(notify: false);
$this->submit(notify: true);
}
public function instantSave()
@@ -76,16 +76,13 @@ class StackForm extends Component
{
try {
$this->validate();
$isValid = validateComposeFile($this->service->docker_compose_raw, $this->service->server->id);
if ($isValid !== 'OK') {
throw new \Exception("Invalid docker-compose file.\n$isValid");
}
$this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->dispatch('refreshEnvs');
$this->dispatch('refreshServices');
$notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -26,6 +26,8 @@ class Destination extends Component
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'loadData',
"echo-private:team.{$teamId},ServiceStatusChanged" => 'mount',
'refresh' => 'mount',
];
}
@@ -79,7 +81,7 @@ class Destination extends Component
$deployment_uuid = new Cuid2;
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
queue_application_deployment(
$result = queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->resource,
server: $server,
@@ -87,6 +89,11 @@ class Destination extends Component
only_this_server: true,
no_questions_asked: true,
);
if ($result['status'] === 'skipped') {
$this->dispatch('success', 'Deployment skipped', $result['message']);
return;
}
return redirect()->route('project.application.deployment.show', [
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
@@ -109,22 +116,20 @@ class Destination extends Component
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]);
$this->refreshServers();
$this->resource->refresh();
}
public function refreshServers()
{
GetContainersStatus::run($this->resource->destination->server);
// ContainerStatusJob::dispatchSync($this->resource->destination->server);
$this->loadData();
$this->dispatch('refresh');
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
public function addServer(int $network_id, int $server_id)
{
$this->resource->additional_networks()->attach($network_id, ['server_id' => $server_id]);
$this->loadData();
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
$this->dispatch('refresh');
}
public function removeServer(int $network_id, int $server_id, $password)
@@ -139,7 +144,7 @@ class Destination extends Component
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
$this->dispatch('error', 'You are trying to remove the main server.');
return;
}
@@ -147,6 +152,7 @@ class Destination extends Component
StopApplicationOneServer::run($this->resource, $server);
$this->resource->additional_networks()->detach($network_id, ['server_id' => $server_id]);
$this->loadData();
$this->dispatch('refresh');
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
} catch (\Exception $e) {
return handleError($e, $this);

View File

@@ -3,10 +3,13 @@
namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class All extends Component
{
use EnvironmentVariableProtection;
public $resource;
public string $resourceClass;
@@ -138,16 +141,47 @@ class All extends Component
private function handleBulkSubmit()
{
$variables = parseEnvFormatToArray($this->variables);
$this->deleteRemovedVariables(false, $variables);
$this->updateOrCreateVariables(false, $variables);
$changesMade = false;
$errorOccurred = false;
// Try to delete removed variables
$deletedCount = $this->deleteRemovedVariables(false, $variables);
if ($deletedCount > 0) {
$changesMade = true;
} elseif ($deletedCount === 0 && $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create variables
$updatedCount = $this->updateOrCreateVariables(false, $variables);
if ($updatedCount > 0) {
$changesMade = true;
}
if ($this->showPreview) {
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
$this->deleteRemovedVariables(true, $previewVariables);
$this->updateOrCreateVariables(true, $previewVariables);
// Try to delete removed preview variables
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
if ($deletedPreviewCount > 0) {
$changesMade = true;
} elseif ($deletedPreviewCount === 0 && $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($previewVariables))->exists()) {
// If we tried to delete but couldn't (due to Docker Compose), mark as error
$errorOccurred = true;
}
// Update or create preview variables
$updatedPreviewCount = $this->updateOrCreateVariables(true, $previewVariables);
if ($updatedPreviewCount > 0) {
$changesMade = true;
}
}
$this->dispatch('success', 'Environment variables updated.');
// Only show success message if changes were actually made and no errors occurred
if ($changesMade && ! $errorOccurred) {
$this->dispatch('success', 'Environment variables updated.');
}
}
private function handleSingleSubmit($data)
@@ -183,19 +217,52 @@ class All extends Component
private function deleteRemovedVariables($isPreview, $variables)
{
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
// Get all environment variables that will be deleted
$variablesToDelete = $this->resource->$method()->whereNotIn('key', array_keys($variables))->get();
// If there are no variables to delete, return 0
if ($variablesToDelete->isEmpty()) {
return 0;
}
// Check if any of these variables are used in Docker Compose
if ($this->resource->type() === 'service' || $this->resource->build_pack === 'dockercompose') {
foreach ($variablesToDelete as $envVar) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($envVar->key, $this->resource->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$envVar->key}' <br><br>Please remove it from the Docker Compose file first.");
return 0;
}
}
}
// If we get here, no variables are used in Docker Compose, so we can delete them
$this->resource->$method()->whereNotIn('key', array_keys($variables))->delete();
return $variablesToDelete->count();
}
private function updateOrCreateVariables($isPreview, $variables)
{
$count = 0;
foreach ($variables as $key => $value) {
if (str($key)->startsWith('SERVICE_FQDN') || str($key)->startsWith('SERVICE_URL')) {
continue;
}
$method = $isPreview ? 'environment_variables_preview' : 'environment_variables';
$found = $this->resource->$method()->where('key', $key)->first();
if ($found) {
if (! $found->is_shown_once && ! $found->is_multiline) {
$found->value = $value;
$found->save();
// Only count as a change if the value actually changed
if ($found->value !== $value) {
$found->value = $value;
$found->save();
$count++;
}
}
} else {
$environment = new EnvironmentVariable;
@@ -208,8 +275,11 @@ class All extends Component
$environment->resourceable_type = $this->resource->getMorphClass();
$environment->save();
$count++;
}
}
return $count;
}
public function refreshEnvs()

View File

@@ -4,10 +4,13 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use App\Models\SharedEnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Livewire\Component;
class Show extends Component
{
use EnvironmentVariableProtection;
public $parameters;
public ModelsEnvironmentVariable|SharedEnvironmentVariable $env;
@@ -40,6 +43,8 @@ class Show extends Component
public bool $is_really_required = false;
public bool $is_redis_credential = false;
protected $listeners = [
'refreshEnvs' => 'refresh',
'refresh',
@@ -65,7 +70,9 @@ class Show extends Component
}
$this->parameters = get_route_parameters();
$this->checkEnvs();
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
$this->is_redis_credential = true;
}
}
public function refresh()
@@ -163,6 +170,7 @@ class Show extends Component
$this->syncData(true);
$this->dispatch('success', 'Environment variable updated.');
$this->dispatch('envsUpdated');
$this->dispatch('configurationChanged');
} catch (\Exception $e) {
return handleError($e);
}
@@ -171,6 +179,17 @@ class Show extends Component
public function delete()
{
try {
// Check if the variable is used in Docker Compose
if ($this->type === 'service' || $this->type === 'application' && $this->env->resource()?->docker_compose) {
[$isUsed, $reason] = $this->isEnvironmentVariableUsedInDockerCompose($this->env->key, $this->env->resource()?->docker_compose);
if ($isUsed) {
$this->dispatch('error', "Cannot delete environment variable '{$this->env->key}' <br><br>Please remove it from the Docker Compose file first.");
return;
}
}
$this->env->delete();
$this->dispatch('environmentVariableDeleted');
$this->dispatch('success', 'Environment variable deleted successfully.');

View File

@@ -13,8 +13,6 @@ class ExecuteContainerCommand extends Component
{
public $selected_container = 'default';
public $container;
public Collection $containers;
public $parameters;
@@ -23,11 +21,9 @@ class ExecuteContainerCommand extends Component
public string $type;
public Server $server;
public Collection $servers;
public bool $hasShell = true;
public bool $isConnecting = false;
protected $rules = [
'server' => 'required',
@@ -76,8 +72,9 @@ class ExecuteContainerCommand extends Component
} elseif (data_get($this->parameters, 'server_uuid')) {
$this->type = 'server';
$this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail();
$this->server = $this->resource;
$this->servers = $this->servers->push($this->resource);
}
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
}
public function loadContainers()
@@ -95,7 +92,7 @@ class ExecuteContainerCommand extends Component
}
foreach ($containers as $container) {
// if container state is running
if (data_get($container, 'State') === 'running') {
if (data_get($container, 'State') === 'running' && $server->isTerminalEnabled()) {
$payload = [
'server' => $server,
'container' => $container,
@@ -104,7 +101,7 @@ class ExecuteContainerCommand extends Component
}
}
} elseif (data_get($this->parameters, 'database_uuid')) {
if ($this->resource->isRunning()) {
if ($this->resource->isRunning() && $server->isTerminalEnabled()) {
$this->containers = $this->containers->push([
'server' => $server,
'container' => [
@@ -114,7 +111,7 @@ class ExecuteContainerCommand extends Component
}
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->resource->applications()->get()->each(function ($application) {
if ($application->isRunning()) {
if ($application->isRunning() && $this->resource->server->isTerminalEnabled()) {
$this->containers->push([
'server' => $this->resource->server,
'container' => [
@@ -135,45 +132,29 @@ class ExecuteContainerCommand extends Component
});
}
}
if ($this->containers->count() > 0) {
$this->container = $this->containers->first();
}
if ($this->containers->count() === 1) {
$this->selected_container = data_get($this->containers->first(), 'container.Names');
}
}
private function checkShellAvailability(Server $server, string $container): bool
{
$escapedContainer = escapeshellarg($container);
try {
instant_remote_process([
"docker exec {$escapedContainer} bash -c 'exit 0' 2>/dev/null || ".
"docker exec {$escapedContainer} sh -c 'exit 0' 2>/dev/null",
], $server);
return true;
} catch (\Throwable) {
return false;
}
}
#[On('connectToServer')]
public function connectToServer()
{
try {
if ($this->server->isForceDisabled()) {
$server = $this->servers->first();
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
$this->hasShell = true;
$this->dispatch(
'send-terminal-command',
false,
data_get($this->server, 'name'),
data_get($this->server, 'uuid')
data_get($server, 'name'),
data_get($server, 'uuid')
);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isConnecting = false;
}
}
@@ -219,11 +200,6 @@ class ExecuteContainerCommand extends Component
throw new \RuntimeException('Server ownership verification failed.');
}
$this->hasShell = $this->checkShellAvailability($server, data_get($container, 'container.Names'));
if (! $this->hasShell) {
return;
}
$this->dispatch(
'send-terminal-command',
true,
@@ -232,6 +208,8 @@ class ExecuteContainerCommand extends Component
);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->isConnecting = false;
}
}

View File

@@ -25,6 +25,8 @@ class Logs extends Component
public Collection $containers;
public array $serverContainers = [];
public $container = [];
public $parameters;
@@ -37,25 +39,60 @@ class Logs extends Component
public $cpu;
public function loadContainers($server_id)
public bool $containersLoaded = false;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
];
}
public function loadAllContainers()
{
try {
$server = $this->servers->firstWhere('id', $server_id);
if (! $server->isFunctional()) {
return;
foreach ($this->servers as $server) {
$this->serverContainers[$server->id] = $this->getContainersForServer($server);
}
$this->containersLoaded = true;
} catch (\Exception $e) {
$this->containersLoaded = true; // Set to true to stop loading spinner
return handleError($e, $this);
}
}
private function getContainersForServer($server)
{
if (! $server->isFunctional()) {
return [];
}
try {
if ($server->isSwarm()) {
$containers = collect([
[
'ID' => $this->resource->uuid,
'Names' => $this->resource->uuid.'_'.$this->resource->uuid,
],
]);
return $containers->toArray();
} else {
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
if ($containers && $containers->count() > 0) {
return $containers->sort()->toArray();
}
return [];
}
$server->containers = $containers->sort();
} catch (\Exception $e) {
return handleError($e, $this);
// Log error but don't fail the entire operation
ray("Error loading containers for server {$server->name}: ".$e->getMessage());
return [];
}
}
@@ -64,6 +101,7 @@ class Logs extends Component
try {
$this->containers = collect();
$this->servers = collect();
$this->serverContainers = [];
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
@@ -71,7 +109,8 @@ class Logs extends Component
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
$server = $this->resource->destination->server;
$this->servers = $this->servers->push($server);
}
foreach ($this->resource->additional_servers as $server) {
if ($server->isFunctional()) {
@@ -87,7 +126,8 @@ class Logs extends Component
$this->resource = $resource;
$this->status = $this->resource->status;
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
$server = $this->resource->destination->server;
$this->servers = $this->servers->push($server);
}
$this->container = $this->resource->uuid;
$this->containers->push($this->container);
@@ -101,7 +141,8 @@ class Logs extends Component
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
});
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
$server = $this->resource->server;
$this->servers = $this->servers->push($server);
}
}
$this->containers = $this->containers->sort();

View File

@@ -35,7 +35,7 @@ class ResourceOperations extends Component
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentUuid = data_get($parameters, 'environment_uuid');
$this->projects = Project::ownedByCurrentTeam()->get();
$this->servers = currentTeam()->servers;
$this->servers = currentTeam()->servers->filter(fn ($server) => ! $server->isBuildServer());
}
public function toggleVolumeCloning(bool $value)

View File

@@ -2,15 +2,22 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Component;
class Add extends Component
{
public $parameters;
#[Locked]
public string $id;
#[Locked]
public string $type;
#[Locked]
public Collection $containerNames;
public string $name;
@@ -21,8 +28,6 @@ class Add extends Component
public ?string $container = '';
protected $listeners = ['clearScheduledTask' => 'clear'];
protected $rules = [
'name' => 'required|string',
'command' => 'required|string',
@@ -60,18 +65,42 @@ class Add extends Component
$this->container = $this->subServiceName;
}
}
$this->dispatch('saveScheduledTask', [
'name' => $this->name,
'command' => $this->command,
'frequency' => $this->frequency,
'container' => $this->container,
]);
$this->saveScheduledTask();
$this->clear();
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function saveScheduledTask()
{
try {
$task = new ScheduledTask;
$task->name = $this->name;
$task->command = $this->command;
$task->frequency = $this->frequency;
$task->container = $this->container;
$task->team_id = currentTeam()->id;
switch ($this->type) {
case 'application':
$task->application_id = $this->id;
break;
case 'standalone-postgresql':
$task->standalone_postgresql_id = $this->id;
break;
case 'service':
$task->service_id = $this->id;
break;
}
$task->save();
$this->dispatch('refreshTasks');
$this->dispatch('success', 'Scheduled task added.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function clear()
{
$this->name = '';

View File

@@ -2,22 +2,23 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
use Illuminate\Support\Collection;
use Livewire\Attributes\Locked;
use Livewire\Attributes\On;
use Livewire\Component;
class All extends Component
{
#[Locked]
public $resource;
#[Locked]
public array $parameters;
public Collection $containerNames;
public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
public function mount()
{
$this->parameters = get_route_parameters();
@@ -35,37 +36,9 @@ class All extends Component
}
}
#[On('refreshTasks')]
public function refreshTasks()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
$task = new ScheduledTask;
$task->name = $data['name'];
$task->command = $data['command'];
$task->frequency = $data['frequency'];
$task->container = $data['container'];
$task->team_id = currentTeam()->id;
switch ($this->resource->type()) {
case 'application':
$task->application_id = $this->resource->id;
break;
case 'standalone-postgresql':
$task->standalone_postgresql_id = $this->resource->id;
break;
case 'service':
$task->service_id = $this->resource->id;
break;
}
$task->save();
$this->refreshTasks();
$this->dispatch('success', 'Scheduled task added.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -46,6 +46,15 @@ class Show extends Component
#[Locked]
public string $task_uuid;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
];
}
public function mount(string $task_uuid, string $project_uuid, string $environment_uuid, ?string $application_uuid = null, ?string $service_uuid = null)
{
try {
@@ -133,9 +142,9 @@ class Show extends Component
$this->task->delete();
if ($this->type === 'application') {
return redirect()->route('project.application.configuration', $this->parameters, $this->task->name);
return redirect()->route('project.application.scheduled-tasks.show', $this->parameters);
} else {
return redirect()->route('project.service.configuration', $this->parameters, $this->task->name);
return redirect()->route('project.service.scheduled-tasks.show', $this->parameters);
}
} catch (\Exception $e) {
return handleError($e);

View File

@@ -44,6 +44,9 @@ class Terminal extends Component
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if (! $server->isTerminalEnabled() || $server->isForceDisabled()) {
abort(403, 'Terminal access is disabled on this server.');
}
if ($isContainer) {
// Validate container identifier format (alphanumeric, dashes, and underscores only)
@@ -65,11 +68,16 @@ class Terminal extends Component
// 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'");
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi';
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'");
} 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');
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi';
$command = SshMultiplexingHelper::generateSshCommand($server, $shellCommand);
}
// ssh command is sent back to frontend then to websocket
// this is done because the websocket connection is not available here
// a better solution would be to remove websocket on NodeJS and work with something like

View File

@@ -29,8 +29,6 @@ class Webhooks extends Component
public function mount()
{
// ray()->clearAll();
// ray()->showQueries();
$this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhookSecret = data_get($this->resource, 'manual_webhook_secret_github');