Merge branch 'next' into feature/signoz
This commit is contained in:
commit
72302d893e
253 changed files with 6686 additions and 2381 deletions
|
|
@ -21,7 +21,9 @@ Coolify implements **defense-in-depth security** with multiple layers of protect
|
|||
- **Supported Providers**:
|
||||
- Google OAuth
|
||||
- Microsoft Azure AD
|
||||
- Clerk
|
||||
- Authentik
|
||||
- Discord
|
||||
- GitHub (via GitHub Apps)
|
||||
- GitLab
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ alwaysApply: false
|
|||
- **Purpose**: OAuth provider integration
|
||||
- **Providers**:
|
||||
- GitHub, GitLab, Google
|
||||
- Microsoft Azure, Authentik
|
||||
- Microsoft Azure, Authentik, Discord, Clerk
|
||||
- Custom OAuth implementations
|
||||
|
||||
## Background Processing
|
||||
|
|
|
|||
202
CHANGELOG.md
202
CHANGELOG.md
|
|
@ -2,7 +2,190 @@ # Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.0.0-beta.419] - 2025-06-16
|
||||
## [unreleased]
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(bump)* Update composer deps
|
||||
- *(version)* Bump Coolify version to 4.0.0-beta.420.6
|
||||
|
||||
## [4.0.0-beta.420.5] - 2025-07-08
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(scheduling)* Add command to manually run scheduled database backups and tasks with options for chunking, delays, and dry runs
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(versions)* Update coolify version numbers in versions.json and constants.php to 4.0.0-beta.420.5 and 4.0.0-beta.420.6
|
||||
- *(database)* Ensure internal port defaults correctly for unsupported database types in StartDatabaseProxy
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(postgresql)* Improve layout and spacing in SSL and Proxy configuration sections for better UI consistency
|
||||
|
||||
## [4.0.0-beta.420.4] - 2025-07-08
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(service)* Update Postiz compose configuration for improved server availability
|
||||
- *(install.sh)* Use IPV4_PUBLIC_IP variable in output instead of repeated curl
|
||||
- *(env)* Generate literal env variables better
|
||||
- *(deployment)* Update x-data initialization in deployment view for improved functionality
|
||||
- *(deployment)* Enhance COOLIFY_URL and COOLIFY_FQDN variable generation for better compatibility
|
||||
- *(deployment)* Improve docker-compose domain handling and environment variable generation
|
||||
- *(deployment)* Refactor domain parsing and environment variable generation using Spatie URL library
|
||||
- *(deployment)* Update COOLIFY_URL and COOLIFY_FQDN generation to use Spatie URL library for improved accuracy
|
||||
- *(scheduling)* Change redis cleanup command frequency from hourly to weekly for better resource management
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(previews)* Streamline preview URL generation by utilizing application method
|
||||
- *(application)* Adjust layout and spacing in general application view for improved UI
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.420.3] - 2025-07-03
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
## [4.0.0-beta.420.2] - 2025-07-03
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(template)* Added excalidraw (#6095)
|
||||
- *(template)* Add excalidraw service configuration with documentation and tags
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(terminal)* Ensure shell execution only uses valid shell if available in terminal command
|
||||
- *(ui)* Improve destination selection description for clarity in resource segregation
|
||||
- *(jobs)* Update middleware to use expireAfter for WithoutOverlapping in multiple job classes
|
||||
- Removing eager loading (#6071)
|
||||
- *(template)* Adjust health check interval and retries for excalidraw service
|
||||
- *(ui)* Env variable settings wrong order
|
||||
- *(service)* Ensure configuration changes are properly tracked and dispatched
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(ui)* Enhance project cloning interface with improved table layout for server and resource selection
|
||||
- *(terminal)* Simplify command construction for SSH execution
|
||||
- *(settings)* Streamline instance admin checks and initialization of settings in Livewire components
|
||||
- *(policy)* Optimize team membership checks in S3StoragePolicy
|
||||
- *(popup)* Improve styling and structure of the small popup component
|
||||
- *(shared)* Enhance FQDN generation logic for services in newParser function
|
||||
- *(redis)* Enhance CleanupRedis command with dry-run option and improved key deletion logic
|
||||
- *(init)* Standardize method naming conventions and improve command structure in Init.php
|
||||
- *(shared)* Improve error handling in getTopLevelNetworks function to return network name on invalid docker-compose.yml
|
||||
- *(database)* Improve error handling for unsupported database types in StartDatabaseProxy
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(versions)* Bump coolify and nightly versions to 4.0.0-beta.420.3 and 4.0.0-beta.420.4 respectively
|
||||
- *(versions)* Update coolify and nightly versions to 4.0.0-beta.420.4 and 4.0.0-beta.420.5 respectively
|
||||
|
||||
## [4.0.0-beta.420.1] - 2025-06-26
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(server)* Prepend 'mux_' to UUID in muxFilename method for consistent naming
|
||||
- *(ui)* Enhance terminal access messaging to clarify server functionality and terminal status
|
||||
- *(database)* Proxy ssl port if ssl is enabled
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(ui)* Separate views for instance settings to separate paths to make it cleaner
|
||||
- *(ui)* Remove unnecessary step3ButtonText attributes from modal confirmation components for cleaner code
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(versions)* Update Coolify versions to 4.0.0-beta.420.2 and 4.0.0-beta.420.3 in multiple files
|
||||
|
||||
## [4.0.0-beta.420] - 2025-06-26
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(service)* Add Miniflux service (#5843)
|
||||
- *(service)* Add Pingvin Share service (#5969)
|
||||
- *(auth)* Add Discord OAuth Provider (#5552)
|
||||
- *(auth)* Add Clerk OAuth Provider (#5553)
|
||||
- *(auth)* Add Zitadel OAuth Provider (#5490)
|
||||
- *(core)* Set custom API rate limit (#5984)
|
||||
- *(service)* Enhance service status handling and UI updates
|
||||
- *(cleanup)* Add functionality to delete teams with no members or servers in CleanupStuckedResources command
|
||||
- *(ui)* Add heart icon and enhance popup messaging for sponsorship support
|
||||
- *(settings)* Add sponsorship popup toggle and corresponding database migration
|
||||
- *(migrations)* Add optimized indexes to activity_log for improved query performance
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(service)* Audiobookshelf healthcheck command (#5993)
|
||||
- *(service)* Downgrade Evolution API phone version (#5977)
|
||||
- *(service)* Pingvinshare-with-clamav
|
||||
- *(ssh)* Scp requires square brackets for ipv6 (#6001)
|
||||
- *(github)* Changing github app breaks the webhook. it does not anymore
|
||||
- *(parser)* Improve FQDN generation and update environment variable handling
|
||||
- *(ui)* Enhance status refresh buttons with loading indicators
|
||||
- *(ui)* Update confirmation button text for stopping database and service
|
||||
- *(routes)* Update middleware for deploy route to use 'api.ability:deploy'
|
||||
- *(ui)* Refine API token creation form and update helper text for clarity
|
||||
- *(ui)* Adjust layout of deployments section for improved alignment
|
||||
- *(ui)* Adjust project grid layout and refine server border styling for better visibility
|
||||
- *(ui)* Update border styling for consistency across components and enhance loading indicators
|
||||
- *(ui)* Add padding to section headers in settings views for improved spacing
|
||||
- *(ui)* Reduce gap between input fields in email settings for better alignment
|
||||
- *(docker)* Conditionally enable gzip compression in Traefik labels based on configuration
|
||||
- *(parser)* Enable gzip compression conditionally for Pocketbase images and streamline service creation logic
|
||||
- *(ui)* Update padding for trademarks policy and enhance spacing in advanced settings section
|
||||
- *(ui)* Correct closing tag for sponsorship link in layout popups
|
||||
- *(ui)* Refine wording in sponsorship donation prompt in layout popups
|
||||
- *(ui)* Update navbar icon color and enhance popup layout for sponsorship support
|
||||
- *(ui)* Add target="_blank" to sponsorship links in layout popups for improved user experience
|
||||
- *(models)* Refine comment wording in User model for clarity on user deletion criteria
|
||||
- *(models)* Improve user deletion logic in User model to handle team member roles and prevent deletion if user is alone in root team
|
||||
- *(ui)* Update wording in sponsorship prompt for clarity and engagement
|
||||
- *(shared)* Refactor gzip handling for Pocketbase in newParser function for improved clarity
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(service)* Update Hoarder to their new name karakeep (#5964)
|
||||
- *(service)* Karakeep naming and formatting
|
||||
- *(service)* Improve miniflux
|
||||
- *(core)* Rename API rate limit ENV
|
||||
- *(ui)* Simplify container selection form in execute-container-command view
|
||||
- *(email)* Streamline SMTP and resend settings logic for improved clarity
|
||||
- *(invitation)* Rename methods for consistency and enhance invitation deletion logic
|
||||
- *(user)* Streamline user deletion process and enhance team management logic
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update changelog
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(service)* Update Evolution API image to the official one (#6031)
|
||||
- *(versions)* Bump coolify versions to v4.0.0-beta.420 and v4.0.0-beta.421
|
||||
- *(dependencies)* Update composer dependencies to latest versions including resend-laravel to ^0.19.0 and aws-sdk-php to 3.347.0
|
||||
- *(versions)* Update Coolify version to 4.0.0-beta.420.1 and add new services (karakeep, miniflux, pingvinshare) to service templates
|
||||
|
||||
## [4.0.0-beta.419] - 2025-06-17
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
|
|
@ -50,6 +233,11 @@ ### 🚀 Features
|
|||
- *(proxy-dashboard)* Implement ProxyDashboardCacheService to manage Traefik dashboard cache; clear cache on configuration changes and proxy actions
|
||||
- *(terminal-connection)* Enhance terminal connection handling with auto-connect feature and improved status messaging
|
||||
- *(terminal)* Implement resize handling with ResizeObserver for improved terminal responsiveness
|
||||
- *(migration)* Add is_sentinel_enabled column to server_settings with default true
|
||||
- *(seeder)* Dispatch StartProxy action for each server in ProductionSeeder
|
||||
- *(seeder)* Add CheckAndStartSentinelJob dispatch for each server in ProductionSeeder
|
||||
- *(seeder)* Conditionally dispatch StartProxy action based on proxy check result
|
||||
- *(service)* Update Changedetection template (#5937)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
|
@ -121,6 +309,11 @@ ### 🐛 Bug Fixes
|
|||
- *(terminal)* Now it should work
|
||||
- *(degraded-status)* Remove unnecessary whitespace in badge element for cleaner HTML
|
||||
- *(routes)* Add name to security route for improved route management
|
||||
- *(migration)* Update default value handling for is_sentinel_enabled column in server_settings
|
||||
- *(seeder)* Conditionally dispatch CheckAndStartSentinelJob based on server's sentinel status
|
||||
- *(service)* Disable healthcheck logging for Gotenberg (#6005)
|
||||
- *(service)* Joplin volume name (#5930)
|
||||
- *(server)* Update sentinelUpdatedAt assignment to use server's sentinel_updated_at property
|
||||
|
||||
### 💼 Other
|
||||
|
||||
|
|
@ -198,6 +391,9 @@ ### 🚜 Refactor
|
|||
- *(navigation)* Remove wire:navigate directive from configuration links for cleaner HTML structure
|
||||
- *(proxy)* Update StartProxy calls to use named parameter for async option
|
||||
- *(clone-project)* Enhance server retrieval by including destinations and filtering out build servers
|
||||
- *(ui)* Terminal
|
||||
- *(ui)* Remove terminal header from execute-container-command view
|
||||
- *(ui)* Remove unnecessary padding from deployment, backup, and logs sections
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
|
|
@ -205,6 +401,7 @@ ### 📚 Documentation
|
|||
- *(service)* Add new docs link for zipline (#5912)
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
- Update changelog
|
||||
|
||||
### 🎨 Styling
|
||||
|
||||
|
|
@ -236,6 +433,9 @@ ### ⚙️ Miscellaneous Tasks
|
|||
- *(api)* Update API docs
|
||||
- *(dependencies)* Update package versions in composer.json and composer.lock for improved compatibility and performance
|
||||
- *(dependencies)* Update package versions in package.json and package-lock.json for improved stability and features
|
||||
- *(version)* Update coolify-realtime to version 1.0.9 in docker-compose and versions files
|
||||
- *(version)* Update coolify version to 4.0.0-beta.420 and nightly version to 4.0.0-beta.421
|
||||
- *(service)* Changedetection remove unused code
|
||||
|
||||
## [4.0.0-beta.417] - 2025-05-07
|
||||
|
||||
|
|
|
|||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Coolify is an open-source, self-hostable platform for deploying applications and managing servers - an alternative to Heroku/Netlify/Vercel. It's built with Laravel (PHP) and uses Docker for containerization.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Frontend Development
|
||||
- `npm run dev` - Start Vite development server for frontend assets
|
||||
- `npm run build` - Build frontend assets for production
|
||||
|
||||
### Backend Development
|
||||
- `php artisan serve` - Start Laravel development server
|
||||
- `php artisan migrate` - Run database migrations
|
||||
- `php artisan queue:work` - Start queue worker for background jobs
|
||||
- `php artisan horizon` - Start Laravel Horizon for queue monitoring
|
||||
- `php artisan tinker` - Start interactive PHP REPL
|
||||
|
||||
### Code Quality
|
||||
- `./vendor/bin/pint` - Run Laravel Pint for code formatting
|
||||
- `./vendor/bin/phpstan` - Run PHPStan for static analysis
|
||||
- `./vendor/bin/pest` - Run Pest tests
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Technology Stack
|
||||
- **Backend**: Laravel 12 (PHP 8.4)
|
||||
- **Frontend**: Livewire + Alpine.js + Tailwind CSS
|
||||
- **Database**: PostgreSQL 15
|
||||
- **Cache/Queue**: Redis 7
|
||||
- **Real-time**: Soketi (WebSocket server)
|
||||
- **Containerization**: Docker & Docker Compose
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Core Models
|
||||
- `Application` - Deployed applications with Git integration
|
||||
- `Server` - Remote servers managed by Coolify
|
||||
- `Service` - Docker Compose services
|
||||
- `Database` - Standalone database instances (PostgreSQL, MySQL, MongoDB, Redis, etc.)
|
||||
- `Team` - Multi-tenancy support
|
||||
- `Project` - Grouping of environments and resources
|
||||
|
||||
#### Job System
|
||||
- Uses Laravel Horizon for queue management
|
||||
- Key jobs: `ApplicationDeploymentJob`, `ServerCheckJob`, `DatabaseBackupJob`
|
||||
- `ScheduledJobManager` and `ServerResourceManager` handle job scheduling
|
||||
|
||||
#### Deployment Flow
|
||||
1. Git webhook triggers deployment
|
||||
2. `ApplicationDeploymentJob` handles build and deployment
|
||||
3. Docker containers are managed on target servers
|
||||
4. Proxy configuration (Nginx/Traefik) is updated
|
||||
|
||||
#### Server Management
|
||||
- SSH-based server communication via `ExecuteRemoteCommand` trait
|
||||
- Docker installation and management
|
||||
- Proxy configuration generation
|
||||
- Resource monitoring and cleanup
|
||||
|
||||
### Directory Structure
|
||||
- `app/Actions/` - Domain-specific actions (Application, Database, Server, etc.)
|
||||
- `app/Jobs/` - Background queue jobs
|
||||
- `app/Livewire/` - Frontend components (full-stack with Livewire)
|
||||
- `app/Models/` - Eloquent models
|
||||
- `bootstrap/helpers/` - Helper functions for various domains
|
||||
- `database/migrations/` - Database schema evolution
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Organization
|
||||
- Use Actions pattern for complex business logic
|
||||
- Livewire components handle UI and user interactions
|
||||
- Jobs handle asynchronous operations
|
||||
- Traits provide shared functionality (e.g., `ExecuteRemoteCommand`)
|
||||
|
||||
### Testing
|
||||
- Uses Pest for testing framework
|
||||
- Tests located in `tests/` directory
|
||||
|
||||
### Deployment and Docker
|
||||
- Applications are deployed using Docker containers
|
||||
- Configuration generated dynamically based on application settings
|
||||
- Supports multiple deployment targets and proxy configurations
|
||||
|
|
@ -49,7 +49,7 @@ public function handle(Application $application, bool $previewDeployments = fals
|
|||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
CleanupDocker::dispatch($server, false, false);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
|||
$server = data_get($database, 'destination.server');
|
||||
$containerName = data_get($database, 'uuid');
|
||||
$proxyContainerName = "{$database->uuid}-proxy";
|
||||
$isSSLEnabled = $database->enable_ssl ?? false;
|
||||
|
||||
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
|
||||
$databaseType = $database->databaseType();
|
||||
$network = $database->service->uuid;
|
||||
|
|
@ -42,6 +44,12 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
|||
'standalone-mongodb' => 27017,
|
||||
default => throw new \Exception("Unsupported database type: $databaseType"),
|
||||
};
|
||||
if ($isSSLEnabled) {
|
||||
$internalPort = match ($databaseType) {
|
||||
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6380,
|
||||
default => $internalPort,
|
||||
};
|
||||
}
|
||||
|
||||
$configuration_dir = database_proxy_dir($database->uuid);
|
||||
if (isDev()) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class StopDatabase
|
|||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
$server = $database->destination->server;
|
||||
|
|
@ -29,7 +29,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
|
|||
$this->stopContainer($database, $database->uuid, 30);
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
CleanupDocker::dispatch($server, false, false);
|
||||
}
|
||||
|
||||
if ($database->is_public) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public function handle(Server $server, $fromUI = false): bool
|
|||
if ($server->id === 0) {
|
||||
$ip = 'host.docker.internal';
|
||||
}
|
||||
$portsToCheck = ['80', '443'];
|
||||
$portsToCheck = [];
|
||||
|
||||
try {
|
||||
if ($server->proxyType() !== ProxyTypes::NONE->value) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class CleanupDocker
|
|||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Server $server)
|
||||
public function handle(Server $server, bool $deleteUnusedVolumes = false, bool $deleteUnusedNetworks = false)
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
$realtimeImage = config('constants.coolify.realtime_image');
|
||||
|
|
@ -36,11 +36,11 @@ public function handle(Server $server)
|
|||
"docker images --filter before=$realtimeImageWithoutPrefixVersion --filter reference=$realtimeImageWithoutPrefix | grep $realtimeImageWithoutPrefix | awk '{print $3}' | xargs -r docker rmi -f",
|
||||
];
|
||||
|
||||
if ($server->settings->delete_unused_volumes) {
|
||||
if ($deleteUnusedVolumes) {
|
||||
$commands[] = 'docker volume prune -af';
|
||||
}
|
||||
|
||||
if ($server->settings->delete_unused_networks) {
|
||||
if ($deleteUnusedNetworks) {
|
||||
$commands[] = 'docker network prune -f';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public function handle($manual_update = false)
|
|||
if (! $this->server) {
|
||||
return;
|
||||
}
|
||||
CleanupDocker::dispatch($this->server);
|
||||
CleanupDocker::dispatch($this->server, false, false);
|
||||
$this->latestVersion = get_latest_version_of_coolify();
|
||||
$this->currentVersion = config('constants.coolify.version');
|
||||
if (! $manual_update) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class DeleteService
|
|||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
|
||||
public function handle(Service $service, bool $deleteVolumes, bool $deleteConnectedNetworks, bool $deleteConfigurations, bool $dockerCleanup)
|
||||
{
|
||||
try {
|
||||
$server = data_get($service, 'server');
|
||||
|
|
@ -71,7 +71,7 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet
|
|||
$service->forceDelete();
|
||||
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
CleanupDocker::dispatch($server, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
|
|||
StopService::run(service: $service, dockerCleanup: false);
|
||||
}
|
||||
$service->saveComposeConfigs();
|
||||
$service->isConfigurationChanged(save: true);
|
||||
$commands[] = 'cd '.$service->workdir();
|
||||
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
|
||||
if ($pullLatestImages) {
|
||||
|
|
@ -41,6 +42,6 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s
|
|||
}
|
||||
}
|
||||
|
||||
return remote_process($commands, $service->server, type_uuid: $service->uuid);
|
||||
return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class StopService
|
|||
|
||||
public string $jobQueue = 'high';
|
||||
|
||||
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
|
||||
public function handle(Service $service, bool $deleteConnectedNetworks = false, bool $dockerCleanup = true)
|
||||
{
|
||||
try {
|
||||
$server = $service->destination->server;
|
||||
|
|
@ -36,11 +36,11 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $
|
|||
$this->stopContainersInParallel($containersToStop, $server);
|
||||
}
|
||||
|
||||
if ($isDeleteOperation) {
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->deleteConnectedNetworks();
|
||||
}
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
CleanupDocker::dispatch($server, false, false);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $e->getMessage();
|
||||
|
|
|
|||
|
|
@ -7,26 +7,270 @@
|
|||
|
||||
class CleanupRedis extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:redis';
|
||||
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}';
|
||||
|
||||
protected $description = 'Cleanup Redis';
|
||||
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$redis = Redis::connection('horizon');
|
||||
$keys = $redis->keys('*');
|
||||
$prefix = config('horizon.prefix');
|
||||
$dryRun = $this->option('dry-run');
|
||||
$skipOverlapping = $this->option('skip-overlapping');
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('DRY RUN MODE - No data will be deleted');
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
$totalKeys = 0;
|
||||
|
||||
// Get all keys with the horizon prefix
|
||||
$keys = $redis->keys('*');
|
||||
$totalKeys = count($keys);
|
||||
|
||||
$this->info("Scanning {$totalKeys} keys for cleanup...");
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
$type = $redis->command('type', [$keyWithoutPrefix]);
|
||||
|
||||
// Handle hash-type keys (individual jobs)
|
||||
if ($type === 5) {
|
||||
$data = $redis->command('hgetall', [$keyWithoutPrefix]);
|
||||
$status = data_get($data, 'status');
|
||||
if ($status === 'completed') {
|
||||
$redis->command('del', [$keyWithoutPrefix]);
|
||||
if ($this->shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun)) {
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
// Handle other key types (metrics, lists, etc.)
|
||||
else {
|
||||
if ($this->shouldDeleteOtherKey($redis, $keyWithoutPrefix, $key, $dryRun)) {
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up overlapping queues if not skipped
|
||||
if (! $skipOverlapping) {
|
||||
$this->info('Cleaning up overlapping queues...');
|
||||
$overlappingCleaned = $this->cleanupOverlappingQueues($redis, $prefix, $dryRun);
|
||||
$deletedCount += $overlappingCleaned;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys");
|
||||
} else {
|
||||
$this->info("Deleted {$deletedCount} out of {$totalKeys} keys");
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldDeleteHashKey($redis, $keyWithoutPrefix, $dryRun)
|
||||
{
|
||||
$data = $redis->command('hgetall', [$keyWithoutPrefix]);
|
||||
$status = data_get($data, 'status');
|
||||
|
||||
// Delete completed and failed jobs
|
||||
if (in_array($status, ['completed', 'failed'])) {
|
||||
if ($dryRun) {
|
||||
$this->line("Would delete job: {$keyWithoutPrefix} (status: {$status})");
|
||||
} else {
|
||||
$redis->command('del', [$keyWithoutPrefix]);
|
||||
$this->line("Deleted job: {$keyWithoutPrefix} (status: {$status})");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function shouldDeleteOtherKey($redis, $keyWithoutPrefix, $fullKey, $dryRun)
|
||||
{
|
||||
// Clean up various Horizon data structures
|
||||
$patterns = [
|
||||
'recent_jobs' => 'Recent jobs list',
|
||||
'failed_jobs' => 'Failed jobs list',
|
||||
'completed_jobs' => 'Completed jobs list',
|
||||
'job_classes' => 'Job classes metrics',
|
||||
'queues' => 'Queue metrics',
|
||||
'processes' => 'Process metrics',
|
||||
'supervisors' => 'Supervisor data',
|
||||
'metrics' => 'General metrics',
|
||||
'workload' => 'Workload data',
|
||||
];
|
||||
|
||||
foreach ($patterns as $pattern => $description) {
|
||||
if (str_contains($keyWithoutPrefix, $pattern)) {
|
||||
if ($dryRun) {
|
||||
$this->line("Would delete {$description}: {$keyWithoutPrefix}");
|
||||
} else {
|
||||
$redis->command('del', [$keyWithoutPrefix]);
|
||||
$this->line("Deleted {$description}: {$keyWithoutPrefix}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old timestamped data (older than 7 days)
|
||||
if (preg_match('/(\d{10})/', $keyWithoutPrefix, $matches)) {
|
||||
$timestamp = (int) $matches[1];
|
||||
$weekAgo = now()->subDays(7)->timestamp;
|
||||
|
||||
if ($timestamp < $weekAgo) {
|
||||
if ($dryRun) {
|
||||
$this->line("Would delete old timestamped data: {$keyWithoutPrefix}");
|
||||
} else {
|
||||
$redis->command('del', [$keyWithoutPrefix]);
|
||||
$this->line("Deleted old timestamped data: {$keyWithoutPrefix}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function cleanupOverlappingQueues($redis, $prefix, $dryRun)
|
||||
{
|
||||
$cleanedCount = 0;
|
||||
$queueKeys = [];
|
||||
|
||||
// Find all queue-related keys
|
||||
$allKeys = $redis->keys('*');
|
||||
foreach ($allKeys as $key) {
|
||||
$keyWithoutPrefix = str_replace($prefix, '', $key);
|
||||
if (str_contains($keyWithoutPrefix, 'queue:') || preg_match('/queues?[:\-]/', $keyWithoutPrefix)) {
|
||||
$queueKeys[] = $keyWithoutPrefix;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Found '.count($queueKeys).' queue-related keys');
|
||||
|
||||
// Group queues by name pattern to find duplicates
|
||||
$queueGroups = [];
|
||||
foreach ($queueKeys as $queueKey) {
|
||||
// Extract queue name (remove timestamps, suffixes)
|
||||
$baseName = preg_replace('/[:\-]\d+$/', '', $queueKey);
|
||||
$baseName = preg_replace('/[:\-](pending|reserved|delayed|processing)$/', '', $baseName);
|
||||
|
||||
if (! isset($queueGroups[$baseName])) {
|
||||
$queueGroups[$baseName] = [];
|
||||
}
|
||||
$queueGroups[$baseName][] = $queueKey;
|
||||
}
|
||||
|
||||
// Process each group for overlaps
|
||||
foreach ($queueGroups as $baseName => $keys) {
|
||||
if (count($keys) > 1) {
|
||||
$cleanedCount += $this->deduplicateQueueGroup($redis, $baseName, $keys, $dryRun);
|
||||
}
|
||||
|
||||
// Also check for duplicate jobs within individual queues
|
||||
foreach ($keys as $queueKey) {
|
||||
$cleanedCount += $this->deduplicateQueueContents($redis, $queueKey, $dryRun);
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
|
||||
private function deduplicateQueueGroup($redis, $baseName, $keys, $dryRun)
|
||||
{
|
||||
$cleanedCount = 0;
|
||||
$this->line("Processing queue group: {$baseName} (".count($keys).' keys)');
|
||||
|
||||
// Sort keys to keep the most recent one
|
||||
usort($keys, function ($a, $b) {
|
||||
// Prefer keys without timestamps (they're usually the main queue)
|
||||
$aHasTimestamp = preg_match('/\d{10}/', $a);
|
||||
$bHasTimestamp = preg_match('/\d{10}/', $b);
|
||||
|
||||
if ($aHasTimestamp && ! $bHasTimestamp) {
|
||||
return 1;
|
||||
}
|
||||
if (! $aHasTimestamp && $bHasTimestamp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// If both have timestamps, prefer the newer one
|
||||
if ($aHasTimestamp && $bHasTimestamp) {
|
||||
preg_match('/(\d{10})/', $a, $aMatches);
|
||||
preg_match('/(\d{10})/', $b, $bMatches);
|
||||
|
||||
return ($bMatches[1] ?? 0) <=> ($aMatches[1] ?? 0);
|
||||
}
|
||||
|
||||
return strcmp($a, $b);
|
||||
});
|
||||
|
||||
// Keep the first (preferred) key, remove others that are empty or redundant
|
||||
$keepKey = array_shift($keys);
|
||||
|
||||
foreach ($keys as $redundantKey) {
|
||||
$type = $redis->command('type', [$redundantKey]);
|
||||
$shouldDelete = false;
|
||||
|
||||
if ($type === 1) { // LIST type
|
||||
$length = $redis->command('llen', [$redundantKey]);
|
||||
if ($length == 0) {
|
||||
$shouldDelete = true;
|
||||
}
|
||||
} elseif ($type === 3) { // SET type
|
||||
$count = $redis->command('scard', [$redundantKey]);
|
||||
if ($count == 0) {
|
||||
$shouldDelete = true;
|
||||
}
|
||||
} elseif ($type === 4) { // ZSET type
|
||||
$count = $redis->command('zcard', [$redundantKey]);
|
||||
if ($count == 0) {
|
||||
$shouldDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldDelete) {
|
||||
if ($dryRun) {
|
||||
$this->line(" Would delete empty queue: {$redundantKey}");
|
||||
} else {
|
||||
$redis->command('del', [$redundantKey]);
|
||||
$this->line(" Deleted empty queue: {$redundantKey}");
|
||||
}
|
||||
$cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
|
||||
private function deduplicateQueueContents($redis, $queueKey, $dryRun)
|
||||
{
|
||||
$cleanedCount = 0;
|
||||
$type = $redis->command('type', [$queueKey]);
|
||||
|
||||
if ($type === 1) { // LIST type - common for job queues
|
||||
$length = $redis->command('llen', [$queueKey]);
|
||||
if ($length > 1) {
|
||||
$items = $redis->command('lrange', [$queueKey, 0, -1]);
|
||||
$uniqueItems = array_unique($items);
|
||||
|
||||
if (count($uniqueItems) < count($items)) {
|
||||
$duplicates = count($items) - count($uniqueItems);
|
||||
|
||||
if ($dryRun) {
|
||||
$this->line(" Would remove {$duplicates} duplicate jobs from queue: {$queueKey}");
|
||||
} else {
|
||||
// Rebuild the list with unique items
|
||||
$redis->command('del', [$queueKey]);
|
||||
foreach (array_reverse($uniqueItems) as $item) {
|
||||
$redis->command('lpush', [$queueKey, $item]);
|
||||
}
|
||||
$this->line(" Removed {$duplicates} duplicate jobs from queue: {$queueKey}");
|
||||
}
|
||||
$cleanedCount += $duplicates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupStuckedResources extends Command
|
||||
|
|
@ -36,6 +37,12 @@ public function handle()
|
|||
private function cleanup_stucked_resources()
|
||||
{
|
||||
try {
|
||||
$teams = Team::all()->filter(function ($team) {
|
||||
return $team->members()->count() === 0 && $team->servers()->count() === 0;
|
||||
});
|
||||
foreach ($teams as $team) {
|
||||
$team->delete();
|
||||
}
|
||||
$servers = Server::all()->filter(function ($server) {
|
||||
return $server->isFunctional();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,24 +36,20 @@ public function handle()
|
|||
|
||||
$this->servers = Server::all();
|
||||
if (! isCloud()) {
|
||||
$this->send_alive_signal();
|
||||
$this->sendAliveSignal();
|
||||
get_public_ips();
|
||||
}
|
||||
|
||||
// Backward compatibility
|
||||
$this->replace_slash_in_environment_name();
|
||||
$this->restore_coolify_db_backup();
|
||||
$this->update_user_emails();
|
||||
$this->replaceSlashInEnvironmentName();
|
||||
$this->restoreCoolifyDbBackup();
|
||||
$this->updateUserEmails();
|
||||
//
|
||||
$this->update_traefik_labels();
|
||||
$this->updateTraefikLabels();
|
||||
if (! isCloud() || $this->option('force-cloud')) {
|
||||
$this->cleanup_unused_network_from_coolify_proxy();
|
||||
}
|
||||
if (isCloud()) {
|
||||
$this->cleanup_unnecessary_dynamic_proxy_configuration();
|
||||
} else {
|
||||
$this->cleanup_in_progress_application_deployments();
|
||||
$this->cleanupUnusedNetworkFromCoolifyProxy();
|
||||
}
|
||||
|
||||
$this->call('cleanup:redis');
|
||||
|
||||
$this->call('cleanup:stucked-resources');
|
||||
|
|
@ -66,33 +62,35 @@ public function handle()
|
|||
|
||||
if (isCloud()) {
|
||||
try {
|
||||
$this->cleanupUnnecessaryDynamicProxyConfiguration();
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! isCloud()) {
|
||||
try {
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
if (! is_null(config('constants.coolify.autoupdate', null))) {
|
||||
if (config('constants.coolify.autoupdate') == true) {
|
||||
echo "Enabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
echo "Disabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
try {
|
||||
$this->cleanupInProgressApplicationDeployments();
|
||||
$this->pullTemplatesFromCDN();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not pull templates from CDN: {$e->getMessage()}\n";
|
||||
}
|
||||
try {
|
||||
$localhost = $this->servers->where('id', 0)->first();
|
||||
$localhost->setupDynamicProxyConfiguration();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
if (! is_null(config('constants.coolify.autoupdate', null))) {
|
||||
if (config('constants.coolify.autoupdate') == true) {
|
||||
echo "Enabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => true]);
|
||||
} else {
|
||||
echo "Disabling auto-update\n";
|
||||
$settings->update(['is_auto_update_enabled' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +115,7 @@ private function optimize()
|
|||
Artisan::call('optimize');
|
||||
}
|
||||
|
||||
private function update_user_emails()
|
||||
private function updateUserEmails()
|
||||
{
|
||||
try {
|
||||
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) {
|
||||
|
|
@ -128,7 +126,7 @@ private function update_user_emails()
|
|||
}
|
||||
}
|
||||
|
||||
private function update_traefik_labels()
|
||||
private function updateTraefikLabels()
|
||||
{
|
||||
try {
|
||||
Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']);
|
||||
|
|
@ -137,7 +135,7 @@ private function update_traefik_labels()
|
|||
}
|
||||
}
|
||||
|
||||
private function cleanup_unnecessary_dynamic_proxy_configuration()
|
||||
private function cleanupUnnecessaryDynamicProxyConfiguration()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
try {
|
||||
|
|
@ -158,7 +156,7 @@ private function cleanup_unnecessary_dynamic_proxy_configuration()
|
|||
}
|
||||
}
|
||||
|
||||
private function cleanup_unused_network_from_coolify_proxy()
|
||||
private function cleanupUnusedNetworkFromCoolifyProxy()
|
||||
{
|
||||
foreach ($this->servers as $server) {
|
||||
if (! $server->isFunctional()) {
|
||||
|
|
@ -197,7 +195,7 @@ private function cleanup_unused_network_from_coolify_proxy()
|
|||
}
|
||||
}
|
||||
|
||||
private function restore_coolify_db_backup()
|
||||
private function restoreCoolifyDbBackup()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
|
||||
try {
|
||||
|
|
@ -223,7 +221,7 @@ private function restore_coolify_db_backup()
|
|||
}
|
||||
}
|
||||
|
||||
private function send_alive_signal()
|
||||
private function sendAliveSignal()
|
||||
{
|
||||
$id = config('app.id');
|
||||
$version = config('constants.coolify.version');
|
||||
|
|
@ -241,7 +239,7 @@ private function send_alive_signal()
|
|||
}
|
||||
}
|
||||
|
||||
private function cleanup_in_progress_application_deployments()
|
||||
private function cleanupInProgressApplicationDeployments()
|
||||
{
|
||||
// Cleanup any failed deployments
|
||||
try {
|
||||
|
|
@ -258,7 +256,7 @@ private function cleanup_in_progress_application_deployments()
|
|||
}
|
||||
}
|
||||
|
||||
private function replace_slash_in_environment_name()
|
||||
private function replaceSlashInEnvironmentName()
|
||||
{
|
||||
if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
|
||||
$environments = Environment::all();
|
||||
|
|
|
|||
247
app/Console/Commands/RunScheduledJobsManually.php
Normal file
247
app/Console/Commands/RunScheduledJobsManually.php
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RunScheduledJobsManually extends Command
|
||||
{
|
||||
protected $signature = 'schedule:run-manual
|
||||
{--type=all : Type of jobs to run (all, backups, tasks)}
|
||||
{--frequency= : Filter by frequency (daily, hourly, weekly, monthly, yearly, or cron expression)}
|
||||
{--chunk=5 : Number of jobs to process in each batch}
|
||||
{--delay=30 : Delay in seconds between batches}
|
||||
{--max= : Maximum number of jobs to process (useful for testing)}
|
||||
{--dry-run : Show what would be executed without actually running jobs}';
|
||||
|
||||
protected $description = 'Manually run scheduled database backups and tasks when cron fails';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$type = $this->option('type');
|
||||
$frequency = $this->option('frequency');
|
||||
$chunkSize = (int) $this->option('chunk');
|
||||
$delay = (int) $this->option('delay');
|
||||
$maxJobs = $this->option('max') ? (int) $this->option('max') : null;
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
$this->info('Starting manual execution of scheduled jobs...'.($dryRun ? ' (DRY RUN)' : ''));
|
||||
$this->info("Type: {$type}".($frequency ? ", Frequency: {$frequency}" : '').", Chunk size: {$chunkSize}, Delay: {$delay}s".($maxJobs ? ", Max jobs: {$maxJobs}" : '').($dryRun ? ', Dry run: enabled' : ''));
|
||||
|
||||
if ($dryRun) {
|
||||
$this->warn('DRY RUN MODE: No jobs will actually be dispatched');
|
||||
}
|
||||
|
||||
if ($type === 'all' || $type === 'backups') {
|
||||
$this->runScheduledBackups($chunkSize, $delay, $maxJobs, $dryRun, $frequency);
|
||||
}
|
||||
|
||||
if ($type === 'all' || $type === 'tasks') {
|
||||
$this->runScheduledTasks($chunkSize, $delay, $maxJobs, $dryRun, $frequency);
|
||||
}
|
||||
|
||||
$this->info('Completed manual execution of scheduled jobs.'.($dryRun ? ' (DRY RUN)' : ''));
|
||||
}
|
||||
|
||||
private function runScheduledBackups(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void
|
||||
{
|
||||
$this->info('Processing scheduled database backups...');
|
||||
|
||||
$query = ScheduledDatabaseBackup::where('enabled', true);
|
||||
|
||||
if ($frequency) {
|
||||
$query->where(function ($q) use ($frequency) {
|
||||
// Handle human-readable frequency strings
|
||||
if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) {
|
||||
$q->where('frequency', $frequency);
|
||||
} else {
|
||||
// Handle cron expressions
|
||||
$q->where('frequency', $frequency);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scheduled_backups = $query->get();
|
||||
|
||||
if ($scheduled_backups->isEmpty()) {
|
||||
$this->info('No enabled scheduled backups found'.($frequency ? " with frequency '{$frequency}'" : '').'.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$finalScheduledBackups = collect();
|
||||
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (blank(data_get($scheduled_backup, 'database'))) {
|
||||
$this->warn("Deleting backup {$scheduled_backup->id} - missing database");
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$server = $scheduled_backup->server();
|
||||
if (blank($server)) {
|
||||
$this->warn("Deleting backup {$scheduled_backup->id} - missing server");
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
$this->warn("Skipping backup {$scheduled_backup->id} - server not functional");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
$this->warn("Skipping backup {$scheduled_backup->id} - subscription not paid");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$finalScheduledBackups->push($scheduled_backup);
|
||||
}
|
||||
|
||||
if ($maxJobs && $finalScheduledBackups->count() > $maxJobs) {
|
||||
$finalScheduledBackups = $finalScheduledBackups->take($maxJobs);
|
||||
$this->info("Limited to {$maxJobs} scheduled backups for testing");
|
||||
}
|
||||
|
||||
$this->info("Found {$finalScheduledBackups->count()} valid scheduled backups to process".($frequency ? " with frequency '{$frequency}'" : ''));
|
||||
|
||||
$chunks = $finalScheduledBackups->chunk($chunkSize);
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
$this->info('Processing backup batch '.($index + 1).' of '.$chunks->count()." ({$chunk->count()} items)");
|
||||
|
||||
foreach ($chunk as $scheduled_backup) {
|
||||
try {
|
||||
if ($dryRun) {
|
||||
$this->info("🔍 Would dispatch backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})");
|
||||
} else {
|
||||
DatabaseBackupJob::dispatch($scheduled_backup);
|
||||
$this->info("✓ Dispatched backup job for: {$scheduled_backup->name} (ID: {$scheduled_backup->id}, Frequency: {$scheduled_backup->frequency})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("✗ Failed to dispatch backup job for {$scheduled_backup->id}: ".$e->getMessage());
|
||||
Log::error('Error dispatching backup job: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($index < $chunks->count() - 1 && ! $dryRun) {
|
||||
$this->info("Waiting {$delay} seconds before next batch...");
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function runScheduledTasks(int $chunkSize, int $delay, ?int $maxJobs = null, bool $dryRun = false, ?string $frequency = null): void
|
||||
{
|
||||
$this->info('Processing scheduled tasks...');
|
||||
|
||||
$query = ScheduledTask::where('enabled', true);
|
||||
|
||||
if ($frequency) {
|
||||
$query->where(function ($q) use ($frequency) {
|
||||
// Handle human-readable frequency strings
|
||||
if (in_array($frequency, ['daily', 'hourly', 'weekly', 'monthly', 'yearly', 'every_minute'])) {
|
||||
$q->where('frequency', $frequency);
|
||||
} else {
|
||||
// Handle cron expressions
|
||||
$q->where('frequency', $frequency);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scheduled_tasks = $query->get();
|
||||
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
$this->info('No enabled scheduled tasks found'.($frequency ? " with frequency '{$frequency}'" : '').'.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$finalScheduledTasks = collect();
|
||||
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
$server = $scheduled_task->server();
|
||||
if (blank($server)) {
|
||||
$this->warn("Deleting task {$scheduled_task->id} - missing server");
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
$this->warn("Skipping task {$scheduled_task->id} - server not functional");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
$this->warn("Skipping task {$scheduled_task->id} - subscription not paid");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $service && ! $application) {
|
||||
$this->warn("Deleting task {$scheduled_task->id} - missing service and application");
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($application && str($application->status)->contains('running') === false) {
|
||||
$this->warn("Skipping task {$scheduled_task->id} - application not running");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($service && str($service->status)->contains('running') === false) {
|
||||
$this->warn("Skipping task {$scheduled_task->id} - service not running");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$finalScheduledTasks->push($scheduled_task);
|
||||
}
|
||||
|
||||
if ($maxJobs && $finalScheduledTasks->count() > $maxJobs) {
|
||||
$finalScheduledTasks = $finalScheduledTasks->take($maxJobs);
|
||||
$this->info("Limited to {$maxJobs} scheduled tasks for testing");
|
||||
}
|
||||
|
||||
$this->info("Found {$finalScheduledTasks->count()} valid scheduled tasks to process".($frequency ? " with frequency '{$frequency}'" : ''));
|
||||
|
||||
$chunks = $finalScheduledTasks->chunk($chunkSize);
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
$this->info('Processing task batch '.($index + 1).' of '.$chunks->count()." ({$chunk->count()} items)");
|
||||
|
||||
foreach ($chunk as $scheduled_task) {
|
||||
try {
|
||||
if ($dryRun) {
|
||||
$this->info("🔍 Would dispatch task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})");
|
||||
} else {
|
||||
ScheduledTaskJob::dispatch($scheduled_task);
|
||||
$this->info("✓ Dispatched task job for: {$scheduled_task->name} (ID: {$scheduled_task->id}, Frequency: {$scheduled_task->frequency})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->error("✗ Failed to dispatch task job for {$scheduled_task->id}: ".$e->getMessage());
|
||||
Log::error('Error dispatching task job: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if ($index < $chunks->count() - 1 && ! $dryRun) {
|
||||
$this->info("Waiting {$delay} seconds before next batch...");
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
278
app/Console/Commands/ViewScheduledLogs.php
Normal file
278
app/Console/Commands/ViewScheduledLogs.php
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ViewScheduledLogs extends Command
|
||||
{
|
||||
protected $signature = 'logs:scheduled
|
||||
{--lines=50 : Number of lines to display}
|
||||
{--follow : Follow the log file (tail -f)}
|
||||
{--date= : Specific date (Y-m-d format, defaults to today)}
|
||||
{--task-name= : Filter by task name (partial match)}
|
||||
{--task-id= : Filter by task ID}
|
||||
{--backup-name= : Filter by backup name (partial match)}
|
||||
{--backup-id= : Filter by backup ID}
|
||||
{--errors : View error logs only}
|
||||
{--all : View both normal and error logs}
|
||||
{--hourly : Filter hourly jobs}
|
||||
{--daily : Filter daily jobs}
|
||||
{--weekly : Filter weekly jobs}
|
||||
{--monthly : Filter monthly jobs}
|
||||
{--frequency= : Filter by specific cron expression}';
|
||||
|
||||
protected $description = 'View scheduled backups and tasks logs with optional filtering';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$date = $this->option('date') ?: now()->format('Y-m-d');
|
||||
$logPaths = $this->getLogPaths($date);
|
||||
|
||||
if (empty($logPaths)) {
|
||||
$this->showAvailableLogFiles($date);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = $this->option('lines');
|
||||
$follow = $this->option('follow');
|
||||
|
||||
// Build grep filters
|
||||
$filters = $this->buildFilters();
|
||||
$filterDescription = $this->getFilterDescription();
|
||||
$logTypeDescription = $this->getLogTypeDescription();
|
||||
|
||||
if ($follow) {
|
||||
$this->info("Following {$logTypeDescription} logs for {$date}{$filterDescription} (Press Ctrl+C to stop)...");
|
||||
$this->line('');
|
||||
|
||||
if (count($logPaths) === 1) {
|
||||
$logPath = $logPaths[0];
|
||||
if ($filters) {
|
||||
passthru("tail -f {$logPath} | grep -E '{$filters}'");
|
||||
} else {
|
||||
passthru("tail -f {$logPath}");
|
||||
}
|
||||
} else {
|
||||
// Multiple files - use multitail or tail with process substitution
|
||||
$logPathsStr = implode(' ', $logPaths);
|
||||
if ($filters) {
|
||||
passthru("tail -f {$logPathsStr} | grep -E '{$filters}'");
|
||||
} else {
|
||||
passthru("tail -f {$logPathsStr}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->info("Showing last {$lines} lines of {$logTypeDescription} logs for {$date}{$filterDescription}:");
|
||||
$this->line('');
|
||||
|
||||
if (count($logPaths) === 1) {
|
||||
$logPath = $logPaths[0];
|
||||
if ($filters) {
|
||||
passthru("tail -n {$lines} {$logPath} | grep -E '{$filters}'");
|
||||
} else {
|
||||
passthru("tail -n {$lines} {$logPath}");
|
||||
}
|
||||
} else {
|
||||
// Multiple files - concatenate and sort by timestamp
|
||||
$logPathsStr = implode(' ', $logPaths);
|
||||
if ($filters) {
|
||||
passthru("tail -n {$lines} {$logPathsStr} | sort | grep -E '{$filters}'");
|
||||
} else {
|
||||
passthru("tail -n {$lines} {$logPathsStr} | sort");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getLogPaths(string $date): array
|
||||
{
|
||||
$paths = [];
|
||||
|
||||
if ($this->option('errors')) {
|
||||
// Error logs only
|
||||
$errorPath = storage_path("logs/scheduled-errors-{$date}.log");
|
||||
if (File::exists($errorPath)) {
|
||||
$paths[] = $errorPath;
|
||||
}
|
||||
} elseif ($this->option('all')) {
|
||||
// Both normal and error logs
|
||||
$normalPath = storage_path("logs/scheduled-{$date}.log");
|
||||
$errorPath = storage_path("logs/scheduled-errors-{$date}.log");
|
||||
|
||||
if (File::exists($normalPath)) {
|
||||
$paths[] = $normalPath;
|
||||
}
|
||||
if (File::exists($errorPath)) {
|
||||
$paths[] = $errorPath;
|
||||
}
|
||||
} else {
|
||||
// Normal logs only (default)
|
||||
$normalPath = storage_path("logs/scheduled-{$date}.log");
|
||||
if (File::exists($normalPath)) {
|
||||
$paths[] = $normalPath;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
private function showAvailableLogFiles(string $date): void
|
||||
{
|
||||
$logType = $this->getLogTypeDescription();
|
||||
$this->warn("No {$logType} logs found for date {$date}");
|
||||
|
||||
// Show available log files
|
||||
$normalFiles = File::glob(storage_path('logs/scheduled-*.log'));
|
||||
$errorFiles = File::glob(storage_path('logs/scheduled-errors-*.log'));
|
||||
|
||||
if (! empty($normalFiles) || ! empty($errorFiles)) {
|
||||
$this->info('Available scheduled log files:');
|
||||
|
||||
if (! empty($normalFiles)) {
|
||||
$this->line(' Normal logs:');
|
||||
foreach ($normalFiles as $file) {
|
||||
$basename = basename($file);
|
||||
$this->line(" - {$basename}");
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($errorFiles)) {
|
||||
$this->line(' Error logs:');
|
||||
foreach ($errorFiles as $file) {
|
||||
$basename = basename($file);
|
||||
$this->line(" - {$basename}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getLogTypeDescription(): string
|
||||
{
|
||||
if ($this->option('errors')) {
|
||||
return 'error';
|
||||
} elseif ($this->option('all')) {
|
||||
return 'all';
|
||||
} else {
|
||||
return 'normal';
|
||||
}
|
||||
}
|
||||
|
||||
private function buildFilters(): ?string
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if ($taskName = $this->option('task-name')) {
|
||||
$filters[] = '"task_name":"[^"]*'.preg_quote($taskName, '/').'[^"]*"';
|
||||
}
|
||||
|
||||
if ($taskId = $this->option('task-id')) {
|
||||
$filters[] = '"task_id":'.preg_quote($taskId, '/');
|
||||
}
|
||||
|
||||
if ($backupName = $this->option('backup-name')) {
|
||||
$filters[] = '"backup_name":"[^"]*'.preg_quote($backupName, '/').'[^"]*"';
|
||||
}
|
||||
|
||||
if ($backupId = $this->option('backup-id')) {
|
||||
$filters[] = '"backup_id":'.preg_quote($backupId, '/');
|
||||
}
|
||||
|
||||
// Frequency filters
|
||||
if ($this->option('hourly')) {
|
||||
$filters[] = $this->getFrequencyPattern('hourly');
|
||||
}
|
||||
|
||||
if ($this->option('daily')) {
|
||||
$filters[] = $this->getFrequencyPattern('daily');
|
||||
}
|
||||
|
||||
if ($this->option('weekly')) {
|
||||
$filters[] = $this->getFrequencyPattern('weekly');
|
||||
}
|
||||
|
||||
if ($this->option('monthly')) {
|
||||
$filters[] = $this->getFrequencyPattern('monthly');
|
||||
}
|
||||
|
||||
if ($frequency = $this->option('frequency')) {
|
||||
$filters[] = '"frequency":"'.preg_quote($frequency, '/').'"';
|
||||
}
|
||||
|
||||
return empty($filters) ? null : implode('|', $filters);
|
||||
}
|
||||
|
||||
private function getFrequencyPattern(string $type): string
|
||||
{
|
||||
$patterns = [
|
||||
'hourly' => [
|
||||
'0 \* \* \* \*', // 0 * * * *
|
||||
'@hourly', // @hourly
|
||||
],
|
||||
'daily' => [
|
||||
'0 0 \* \* \*', // 0 0 * * *
|
||||
'@daily', // @daily
|
||||
'@midnight', // @midnight
|
||||
],
|
||||
'weekly' => [
|
||||
'0 0 \* \* [0-6]', // 0 0 * * 0-6 (any day of week)
|
||||
'@weekly', // @weekly
|
||||
],
|
||||
'monthly' => [
|
||||
'0 0 1 \* \*', // 0 0 1 * * (first of month)
|
||||
'@monthly', // @monthly
|
||||
],
|
||||
];
|
||||
|
||||
$typePatterns = $patterns[$type] ?? [];
|
||||
|
||||
// For grep, we need to match the frequency field in JSON
|
||||
return '"frequency":"('.implode('|', $typePatterns).')"';
|
||||
}
|
||||
|
||||
private function getFilterDescription(): string
|
||||
{
|
||||
$descriptions = [];
|
||||
|
||||
if ($taskName = $this->option('task-name')) {
|
||||
$descriptions[] = "task name: {$taskName}";
|
||||
}
|
||||
|
||||
if ($taskId = $this->option('task-id')) {
|
||||
$descriptions[] = "task ID: {$taskId}";
|
||||
}
|
||||
|
||||
if ($backupName = $this->option('backup-name')) {
|
||||
$descriptions[] = "backup name: {$backupName}";
|
||||
}
|
||||
|
||||
if ($backupId = $this->option('backup-id')) {
|
||||
$descriptions[] = "backup ID: {$backupId}";
|
||||
}
|
||||
|
||||
// Frequency filters
|
||||
if ($this->option('hourly')) {
|
||||
$descriptions[] = 'hourly jobs';
|
||||
}
|
||||
|
||||
if ($this->option('daily')) {
|
||||
$descriptions[] = 'daily jobs';
|
||||
}
|
||||
|
||||
if ($this->option('weekly')) {
|
||||
$descriptions[] = 'weekly jobs';
|
||||
}
|
||||
|
||||
if ($this->option('monthly')) {
|
||||
$descriptions[] = 'monthly jobs';
|
||||
}
|
||||
|
||||
if ($frequency = $this->option('frequency')) {
|
||||
$descriptions[] = "frequency: {$frequency}";
|
||||
}
|
||||
|
||||
return empty($descriptions) ? '' : ' (filtered by '.implode(', ', $descriptions).')';
|
||||
}
|
||||
}
|
||||
|
|
@ -6,23 +6,16 @@
|
|||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\RegenerateSslCertJob;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerPatchCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
use App\Jobs\ServerResourceManager;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
|
|
@ -52,7 +45,7 @@ protected function schedule(Schedule $schedule): void
|
|||
}
|
||||
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
$this->scheduleInstance->command('cleanup:redis')->hourly();
|
||||
$this->scheduleInstance->command('cleanup:redis')->weekly();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
|
|
@ -61,10 +54,10 @@ protected function schedule(Schedule $schedule): void
|
|||
$this->scheduleInstance->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer();
|
||||
|
||||
// Server Jobs
|
||||
$this->checkResources();
|
||||
$this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer();
|
||||
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
// Scheduled Jobs (Backups & Tasks)
|
||||
$this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
|
|
@ -79,12 +72,12 @@ protected function schedule(Schedule $schedule): void
|
|||
$this->scheduleUpdates();
|
||||
|
||||
// Server Jobs
|
||||
$this->checkResources();
|
||||
$this->scheduleInstance->job(new ServerResourceManager)->everyMinute()->onOneServer();
|
||||
|
||||
$this->pullImages();
|
||||
|
||||
$this->checkScheduledBackups();
|
||||
$this->checkScheduledTasks();
|
||||
// Scheduled Jobs (Backups & Tasks)
|
||||
$this->scheduleInstance->job(new ScheduledJobManager)->everyMinute()->onOneServer();
|
||||
|
||||
$this->scheduleInstance->job(new RegenerateSslCertJob)->twiceDaily();
|
||||
|
||||
|
|
@ -135,182 +128,6 @@ private function scheduleUpdates(): void
|
|||
}
|
||||
}
|
||||
|
||||
private function checkResources(): void
|
||||
{
|
||||
if (isCloud()) {
|
||||
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
|
||||
$own = Team::find(0)->servers;
|
||||
$servers = $servers->merge($own);
|
||||
} else {
|
||||
$servers = $this->allServers->get();
|
||||
}
|
||||
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Sentinel check
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
|
||||
// Check container status every minute if Sentinel does not activated
|
||||
if (isCloud()) {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer();
|
||||
}
|
||||
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
|
||||
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
}
|
||||
|
||||
$dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) {
|
||||
$dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency];
|
||||
}
|
||||
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// Server patch check - weekly
|
||||
$this->scheduleInstance->job(new ServerPatchCheckJob($server))->weekly()->timezone($serverTimezone)->onOneServer();
|
||||
|
||||
// Cleanup multiplexed connections every hour
|
||||
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
|
||||
|
||||
// Temporary solution until we have better memory management for Sentinel
|
||||
if ($server->isSentinelEnabled()) {
|
||||
$this->scheduleInstance->job(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
})->daily()->onOneServer();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking resources: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkScheduledBackups(): void
|
||||
{
|
||||
$scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get();
|
||||
if ($scheduled_backups->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$finalScheduledBackups = collect();
|
||||
foreach ($scheduled_backups as $scheduled_backup) {
|
||||
if (blank(data_get($scheduled_backup, 'database'))) {
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
$server = $scheduled_backup->server();
|
||||
if (blank($server)) {
|
||||
$scheduled_backup->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($server->isFunctional() === false) {
|
||||
continue;
|
||||
}
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
continue;
|
||||
}
|
||||
$finalScheduledBackups->push($scheduled_backup);
|
||||
}
|
||||
|
||||
foreach ($finalScheduledBackups as $scheduled_backup) {
|
||||
try {
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$server = $scheduled_backup->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
|
||||
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
$this->scheduleInstance->job(new DatabaseBackupJob(
|
||||
backup: $scheduled_backup
|
||||
))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling backup: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkScheduledTasks(): void
|
||||
{
|
||||
$scheduled_tasks = ScheduledTask::where('enabled', true)->get();
|
||||
if ($scheduled_tasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$finalScheduledTasks = collect();
|
||||
foreach ($scheduled_tasks as $scheduled_task) {
|
||||
$service = $scheduled_task->service;
|
||||
$application = $scheduled_task->application;
|
||||
|
||||
$server = $scheduled_task->server();
|
||||
if (blank($server)) {
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $service && ! $application) {
|
||||
$scheduled_task->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($application && str($application->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
if ($service && str($service->status)->contains('running') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finalScheduledTasks->push($scheduled_task);
|
||||
}
|
||||
|
||||
foreach ($finalScheduledTasks as $scheduled_task) {
|
||||
try {
|
||||
$server = $scheduled_task->server();
|
||||
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
|
||||
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
|
||||
}
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
$this->scheduleInstance->job(new ScheduledTaskJob(
|
||||
task: $scheduled_task
|
||||
))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error scheduling task: '.$e->getMessage());
|
||||
Log::error($e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function commands(): void
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Horizon\Contracts\Silenced;
|
||||
|
||||
class BackupCreated implements ShouldBroadcast
|
||||
class BackupCreated implements ShouldBroadcast, Silenced
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Laravel\Horizon\Contracts\Silenced;
|
||||
|
||||
class ServiceChecked implements ShouldBroadcast
|
||||
class ServiceChecked implements ShouldBroadcast, Silenced
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,11 @@ public static function generateScpCommand(Server $server, string $source, string
|
|||
}
|
||||
|
||||
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
|
||||
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||
if ($server->isIpv6()) {
|
||||
$scp_command .= "{$source} {$server->user}@[{$server->ip}]:{$dest}";
|
||||
} else {
|
||||
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
|
||||
}
|
||||
|
||||
return $scp_command;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1699,10 +1699,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $application,
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -1608,10 +1608,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $database,
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -510,10 +510,10 @@ public function delete_by_uuid(Request $request)
|
|||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $service,
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public function acceptInvitation()
|
|||
}
|
||||
}
|
||||
|
||||
public function revoke_invitation()
|
||||
public function revokeInvitation()
|
||||
{
|
||||
$invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail();
|
||||
$user = User::whereEmail($invitation->email)->firstOrFail();
|
||||
|
|
|
|||
|
|
@ -143,12 +143,13 @@ public function manual(Request $request)
|
|||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
ApplicationPreview::create([
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'bitbucket',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
$result = queue_application_deployment(
|
||||
|
|
|
|||
|
|
@ -175,12 +175,13 @@ public function manual(Request $request)
|
|||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
ApplicationPreview::create([
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'gitea',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
$result = queue_application_deployment(
|
||||
|
|
|
|||
|
|
@ -183,12 +183,13 @@ public function manual(Request $request)
|
|||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
ApplicationPreview::create([
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -202,12 +202,13 @@ public function manual(Request $request)
|
|||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
ApplicationPreview::create([
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'gitlab',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
$result = queue_application_deployment(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
|
|||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
'webhooks/*',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
|
@ -228,7 +229,14 @@ public function __construct(public int $application_deployment_queue_id)
|
|||
|
||||
// Set preview fqdn
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->preview = $this->application->generate_preview_fqdn($this->pull_request_id);
|
||||
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
|
||||
if ($this->preview) {
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->preview->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
$this->preview->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::IN_PROGRESS);
|
||||
}
|
||||
|
|
@ -471,7 +479,7 @@ private function deploy_docker_compose_buildpack()
|
|||
} else {
|
||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
||||
$this->save_environment_variables();
|
||||
if (! is_null($this->env_filename)) {
|
||||
if (filled($this->env_filename)) {
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
$service['env_file'] = [$this->env_filename];
|
||||
|
|
@ -480,7 +488,7 @@ private function deploy_docker_compose_buildpack()
|
|||
});
|
||||
$composeFile['services'] = $services->toArray();
|
||||
}
|
||||
if (is_null($composeFile)) {
|
||||
if (empty($composeFile)) {
|
||||
$this->application_deployment_queue->addLogEntry('Failed to parse docker-compose file.');
|
||||
$this->fail('Failed to parse docker-compose file.');
|
||||
|
||||
|
|
@ -887,10 +895,6 @@ private function check_image_locally_or_remotely()
|
|||
private function save_environment_variables()
|
||||
{
|
||||
$envs = collect([]);
|
||||
$local_branch = $this->branch;
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$local_branch = "pull/{$this->pull_request_id}/head";
|
||||
}
|
||||
$sort = $this->application->settings->is_env_sorting_enabled;
|
||||
if ($sort) {
|
||||
$sorted_environment_variables = $this->application->environment_variables->sortBy('key');
|
||||
|
|
@ -899,6 +903,14 @@ private function save_environment_variables()
|
|||
$sorted_environment_variables = $this->application->environment_variables->sortBy('id');
|
||||
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$sorted_environment_variables = $sorted_environment_variables->filter(function ($env) {
|
||||
return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_');
|
||||
});
|
||||
$sorted_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) {
|
||||
return ! str($env->key)->startsWith('SERVICE_FQDN_') && ! str($env->key)->startsWith('SERVICE_URL_');
|
||||
});
|
||||
}
|
||||
$ports = $this->application->main_port();
|
||||
$coolify_envs = $this->generate_coolify_env_variables();
|
||||
$coolify_envs->each(function ($item, $key) use ($envs) {
|
||||
|
|
@ -908,17 +920,7 @@ private function save_environment_variables()
|
|||
$this->env_filename = '.env';
|
||||
|
||||
foreach ($sorted_environment_variables as $env) {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$envs->push($env->key.'='.$real_value);
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
|
|
@ -930,20 +932,28 @@ private function save_environment_variables()
|
|||
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
|
||||
$envs->push('HOST=0.0.0.0');
|
||||
}
|
||||
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect([]);
|
||||
|
||||
// Generate SERVICE_FQDN & SERVICE_URL for dockercompose
|
||||
foreach ($domains as $forServiceName => $domain) {
|
||||
$parsedDomain = data_get($domain, 'domain');
|
||||
if (filled($parsedDomain)) {
|
||||
$parsedDomain = str($parsedDomain)->explode(',')->first();
|
||||
$coolifyUrl = Url::fromString($parsedDomain);
|
||||
$coolifyScheme = $coolifyUrl->getScheme();
|
||||
$coolifyFqdn = $coolifyUrl->getHost();
|
||||
$coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null);
|
||||
$envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString());
|
||||
$envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->env_filename = ".env-pr-$this->pull_request_id";
|
||||
foreach ($sorted_environment_variables_preview as $env) {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$envs->push($env->key.'='.$real_value);
|
||||
$envs->push($env->key.'='.$env->real_value);
|
||||
}
|
||||
// Add PORT if not exists, use the first port as default
|
||||
if ($this->build_pack !== 'dockercompose') {
|
||||
|
|
@ -956,6 +966,23 @@ private function save_environment_variables()
|
|||
$envs->push('HOST=0.0.0.0');
|
||||
}
|
||||
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$domains = collect(json_decode(data_get($this->preview, 'docker_compose_domains'))) ?? collect([]);
|
||||
|
||||
// Generate SERVICE_FQDN & SERVICE_URL for dockercompose
|
||||
foreach ($domains as $forServiceName => $domain) {
|
||||
$parsedDomain = data_get($domain, 'domain');
|
||||
if (filled($parsedDomain)) {
|
||||
$parsedDomain = str($parsedDomain)->explode(',')->first();
|
||||
$coolifyUrl = Url::fromString($parsedDomain);
|
||||
$coolifyScheme = $coolifyUrl->getScheme();
|
||||
$coolifyFqdn = $coolifyUrl->getHost();
|
||||
$coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null);
|
||||
$envs->push('SERVICE_URL_'.str($forServiceName)->upper().'='.$coolifyUrl->__toString());
|
||||
$envs->push('SERVICE_FQDN_'.str($forServiceName)->upper().'='.$coolifyFqdn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($envs->isEmpty()) {
|
||||
$this->env_filename = null;
|
||||
|
|
@ -1367,9 +1394,16 @@ private function set_coolify_variables()
|
|||
$fqdn = $this->preview->fqdn;
|
||||
}
|
||||
if (isset($fqdn)) {
|
||||
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
|
||||
$url = str($fqdn)->replace('http://', '')->replace('https://', '');
|
||||
$this->coolify_variables .= "COOLIFY_URL={$url} ";
|
||||
$url = Url::fromString($fqdn);
|
||||
$fqdn = $url->getHost();
|
||||
$url = $url->withHost($fqdn)->withPort(null)->__toString();
|
||||
if ((int) $this->application->compose_parsing_version >= 3) {
|
||||
$this->coolify_variables .= "COOLIFY_URL={$url} ";
|
||||
$this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
|
||||
} else {
|
||||
$this->coolify_variables .= "COOLIFY_URL={$fqdn} ";
|
||||
$this->coolify_variables .= "COOLIFY_FQDN={$url} ";
|
||||
}
|
||||
}
|
||||
if (isset($this->application->git_branch)) {
|
||||
$this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
|
||||
|
|
@ -1381,8 +1415,8 @@ private function check_git_if_build_needed()
|
|||
if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
|
||||
$repository = githubApi($this->source, "repos/{$this->customRepository}");
|
||||
$data = data_get($repository, 'data');
|
||||
if (isset($data->id)) {
|
||||
$repository_project_id = $data->id;
|
||||
$repository_project_id = data_get($data, 'id');
|
||||
if (isset($repository_project_id)) {
|
||||
if (blank($this->application->repository_project_id) || $this->application->repository_project_id !== $repository_project_id) {
|
||||
$this->application->repository_project_id = $repository_project_id;
|
||||
$this->application->save();
|
||||
|
|
@ -1715,10 +1749,6 @@ private function generate_compose_file()
|
|||
{
|
||||
$this->create_workdir();
|
||||
$ports = $this->application->main_port();
|
||||
$onlyPort = null;
|
||||
if (count($ports) > 0) {
|
||||
$onlyPort = $ports[0];
|
||||
}
|
||||
$persistent_storages = $this->generate_local_persistent_volumes();
|
||||
$persistent_file_volumes = $this->application->fileStorages()->get();
|
||||
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
|
||||
|
|
@ -2253,9 +2283,10 @@ private function build_image()
|
|||
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
|
||||
}
|
||||
|
||||
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
|
||||
private function graceful_shutdown_container(string $containerName)
|
||||
{
|
||||
try {
|
||||
$timeout = isDev() ? 1 : 30;
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public function __construct() {}
|
|||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()];
|
||||
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -60,9 +62,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public ?S3Storage $s3 = null;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
||||
public string $backup_log_uuid;
|
||||
|
||||
public function __construct(public ScheduledDatabaseBackup $backup)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->timeout = $backup->timeout;
|
||||
|
||||
$this->backup_log_uuid = (string) new Cuid2;
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
|
|
@ -219,12 +228,8 @@ public function handle(): void
|
|||
$this->mongo_root_username = str($rootUsername)->after('MONGO_INITDB_ROOT_USERNAME=')->value();
|
||||
}
|
||||
}
|
||||
\Log::info('MongoDB credentials extracted from environment', [
|
||||
'has_username' => filled($this->mongo_root_username),
|
||||
'has_password' => filled($this->mongo_root_password),
|
||||
]);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
\Log::warning('Failed to extract MongoDB environment variables', ['error' => $e->getMessage()]);
|
||||
// Continue without env vars - will be handled in backup_standalone_mongodb method
|
||||
}
|
||||
}
|
||||
|
|
@ -288,6 +293,7 @@ public function handle(): void
|
|||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
|
|
@ -307,6 +313,7 @@ public function handle(): void
|
|||
$this->backup_file = "/mongo-dump-$databaseName-".Carbon::now()->timestamp.'.tar.gz';
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $databaseName,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
|
|
@ -319,6 +326,7 @@ public function handle(): void
|
|||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
|
|
@ -331,6 +339,7 @@ public function handle(): void
|
|||
}
|
||||
$this->backup_location = $this->backup_dir.$this->backup_file;
|
||||
$this->backup_log = ScheduledDatabaseBackupExecution::create([
|
||||
'uuid' => $this->backup_log_uuid,
|
||||
'database_name' => $database,
|
||||
'filename' => $this->backup_location,
|
||||
'scheduled_database_backup_id' => $this->backup->id,
|
||||
|
|
@ -574,4 +583,18 @@ private function getFullImageName(): string
|
|||
|
||||
return "{$helperImage}:{$latestVersion}";
|
||||
}
|
||||
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
$log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first();
|
||||
|
||||
if ($log) {
|
||||
$log->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'),
|
||||
'size' => 0,
|
||||
'filename' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Actions\Service\DeleteService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
|
|
@ -30,11 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
|||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
||||
public bool $deleteConfigurations = true,
|
||||
public Application|ApplicationPreview|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
|
||||
public bool $deleteVolumes = true,
|
||||
public bool $dockerCleanup = true,
|
||||
public bool $deleteConnectedNetworks = true
|
||||
public bool $deleteConnectedNetworks = true,
|
||||
public bool $deleteConfigurations = true,
|
||||
public bool $dockerCleanup = true
|
||||
) {
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
|
@ -42,9 +43,16 @@ public function __construct(
|
|||
public function handle()
|
||||
{
|
||||
try {
|
||||
// Handle ApplicationPreview instances separately
|
||||
if ($this->resource instanceof ApplicationPreview) {
|
||||
$this->deleteApplicationPreview();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
StopApplication::run($this->resource, previewDeployments: true);
|
||||
StopApplication::run($this->resource, previewDeployments: true, dockerCleanup: $this->dockerCleanup);
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
case 'standalone-redis':
|
||||
|
|
@ -54,11 +62,11 @@ public function handle()
|
|||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
StopDatabase::run($this->resource, true);
|
||||
StopDatabase::run($this->resource, dockerCleanup: $this->dockerCleanup);
|
||||
break;
|
||||
case 'service':
|
||||
StopService::run($this->resource, true);
|
||||
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||
StopService::run($this->resource, $this->deleteConnectedNetworks, $this->dockerCleanup);
|
||||
DeleteService::run($this->resource, $this->deleteVolumes, $this->deleteConnectedNetworks, $this->deleteConfigurations, $this->dockerCleanup);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -70,7 +78,7 @@ public function handle()
|
|||
$this->resource->deleteVolumes();
|
||||
$this->resource->persistentStorages()->delete();
|
||||
}
|
||||
$this->resource->fileStorages()->delete();
|
||||
$this->resource->fileStorages()->delete(); // these are file mounts which should probably have their own flag
|
||||
|
||||
$isDatabase = $this->resource instanceof StandalonePostgresql
|
||||
|| $this->resource instanceof StandaloneRedis
|
||||
|
|
@ -98,10 +106,61 @@ public function handle()
|
|||
if ($this->dockerCleanup) {
|
||||
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
|
||||
if ($server) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
CleanupDocker::dispatch($server, false, false);
|
||||
}
|
||||
}
|
||||
Artisan::queue('cleanup:stucked-resources');
|
||||
}
|
||||
}
|
||||
|
||||
private function deleteApplicationPreview()
|
||||
{
|
||||
$application = $this->resource->application;
|
||||
$server = $application->destination->server;
|
||||
$pull_request_id = $this->resource->pull_request_id;
|
||||
|
||||
// Ensure the preview is soft deleted (may already be done in Livewire component)
|
||||
if (! $this->resource->trashed()) {
|
||||
$this->resource->delete();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $application->id, $pull_request_id)->toArray();
|
||||
$this->stopPreviewContainers($containers, $server);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Log the error but don't fail the job
|
||||
ray('Error stopping preview containers: '.$e->getMessage());
|
||||
}
|
||||
|
||||
// Finally, force delete to trigger resource cleanup
|
||||
$this->resource->forceDelete();
|
||||
}
|
||||
|
||||
private function stopPreviewContainers(array $containers, $server, int $timeout = 30)
|
||||
{
|
||||
if (empty($containers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$containerNames = [];
|
||||
foreach ($containers as $container) {
|
||||
$containerNames[] = str_replace('/', '', $container['Names']);
|
||||
}
|
||||
|
||||
$containerList = implode(' ', array_map('escapeshellarg', $containerNames));
|
||||
$commands = [
|
||||
"docker stop --time=$timeout $containerList",
|
||||
"docker rm -f $containerList",
|
||||
];
|
||||
|
||||
instant_remote_process(
|
||||
command: $commands,
|
||||
server: $server,
|
||||
throwError: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,15 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('docker-cleanup-'.$this->server->uuid))->expireAfter(600)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server, public bool $manualCleanup = false) {}
|
||||
public function __construct(
|
||||
public Server $server,
|
||||
public bool $manualCleanup = false,
|
||||
public bool $deleteUnusedVolumes = false,
|
||||
public bool $deleteUnusedNetworks = false
|
||||
) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
|
|
@ -50,7 +55,11 @@ public function handle(): void
|
|||
$this->usageBefore = $this->server->getDiskUsage();
|
||||
|
||||
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$cleanup_log = CleanupDocker::run(
|
||||
server: $this->server,
|
||||
deleteUnusedVolumes: $this->deleteUnusedVolumes,
|
||||
deleteUnusedNetworks: $this->deleteUnusedNetworks
|
||||
);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
$message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
|
||||
|
||||
|
|
@ -67,7 +76,11 @@ public function handle(): void
|
|||
}
|
||||
|
||||
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$cleanup_log = CleanupDocker::run(
|
||||
server: $this->server,
|
||||
deleteUnusedVolumes: $this->deleteUnusedVolumes,
|
||||
deleteUnusedNetworks: $this->deleteUnusedNetworks
|
||||
);
|
||||
$message = 'Docker cleanup job executed successfully, but no disk usage could be determined.';
|
||||
|
||||
$this->execution_log->update([
|
||||
|
|
@ -81,7 +94,11 @@ public function handle(): void
|
|||
}
|
||||
|
||||
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
|
||||
$cleanup_log = CleanupDocker::run(server: $this->server);
|
||||
$cleanup_log = CleanupDocker::run(
|
||||
server: $this->server,
|
||||
deleteUnusedVolumes: $this->deleteUnusedVolumes,
|
||||
deleteUnusedNetworks: $this->deleteUnusedNetworks
|
||||
);
|
||||
$usageAfter = $this->server->getDiskUsage();
|
||||
$diskSaved = $this->usageBefore - $usageAfter;
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@
|
|||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Horizon\Contracts\Silenced;
|
||||
|
||||
class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
||||
class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('push-server-update-'.$this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('push-server-update-'.$this->server->uuid))->expireAfter(30)->dontRelease()];
|
||||
}
|
||||
|
||||
public function backoff(): int
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('restart-proxy-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
|
|
|||
229
app/Jobs/ScheduledJobManager.php
Normal file
229
app/Jobs/ScheduledJobManager.php
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ScheduledJobManager implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The time when this job execution started.
|
||||
* Used to ensure all scheduled items are evaluated against the same point in time.
|
||||
*/
|
||||
private ?Carbon $executionTime = null;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue($this->determineQueue());
|
||||
}
|
||||
|
||||
private function determineQueue(): string
|
||||
{
|
||||
$preferredQueue = 'crons';
|
||||
$fallbackQueue = 'high';
|
||||
|
||||
$configuredQueues = explode(',', env('HORIZON_QUEUES', 'high,default'));
|
||||
|
||||
return in_array($preferredQueue, $configuredQueues) ? $preferredQueue : $fallbackQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping('scheduled-job-manager'))
|
||||
->releaseAfter(60), // Release the lock after 60 seconds if job fails
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Freeze the execution time at the start of the job
|
||||
$this->executionTime = Carbon::now();
|
||||
|
||||
// Process backups - don't let failures stop task processing
|
||||
try {
|
||||
$this->processScheduledBackups();
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Failed to process scheduled backups', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Process tasks - don't let failures stop the job manager
|
||||
try {
|
||||
$this->processScheduledTasks();
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Failed to process scheduled tasks', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processScheduledBackups(): void
|
||||
{
|
||||
$backups = ScheduledDatabaseBackup::with(['database'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
foreach ($backups as $backup) {
|
||||
try {
|
||||
// Apply the same filtering logic as the original
|
||||
if (! $this->shouldProcessBackup($backup)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$server = $backup->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$frequency = $backup->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||
DatabaseBackupJob::dispatch($backup);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing backup', [
|
||||
'backup_id' => $backup->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processScheduledTasks(): void
|
||||
{
|
||||
$tasks = ScheduledTask::with(['service', 'application'])
|
||||
->where('enabled', true)
|
||||
->get();
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
try {
|
||||
if (! $this->shouldProcessTask($task)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$server = $task->server();
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', config('app.timezone'));
|
||||
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
$frequency = $task->frequency;
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||
ScheduledTaskJob::dispatch($task);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing task', [
|
||||
'task_id' => $task->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldProcessBackup(ScheduledDatabaseBackup $backup): bool
|
||||
{
|
||||
if (blank(data_get($backup, 'database'))) {
|
||||
$backup->delete();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$server = $backup->server();
|
||||
if (blank($server)) {
|
||||
$backup->delete();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function shouldProcessTask(ScheduledTask $task): bool
|
||||
{
|
||||
$service = $task->service;
|
||||
$application = $task->application;
|
||||
|
||||
$server = $task->server();
|
||||
if (blank($server)) {
|
||||
$task->delete();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $service && ! $application) {
|
||||
$task->delete();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($application && str($application->status)->contains('running') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($service && str($service->status)->contains('running') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function shouldRunNow(string $frequency, string $timezone): bool
|
||||
{
|
||||
$cron = new CronExpression($frequency);
|
||||
|
||||
// Use the frozen execution time, not the current time
|
||||
// Fallback to current time if execution time is not set (shouldn't happen)
|
||||
$baseTime = $this->executionTime ?? Carbon::now();
|
||||
$executionTime = $baseTime->copy()->setTimezone($timezone);
|
||||
|
||||
return $cron->isDue($executionTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('server-check-'.$this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('server-check-'.$this->server->uuid))->expireAfter(60)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ class ServerPatchCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public $tries = 3;
|
||||
|
||||
public $timeout = 600; // 10 minutes timeout
|
||||
public $timeout = 600;
|
||||
|
||||
public function middleware(): array
|
||||
{
|
||||
return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->dontRelease()];
|
||||
return [(new WithoutOverlapping('server-patch-check-'.$this->server->uuid))->expireAfter(600)->dontRelease()];
|
||||
}
|
||||
|
||||
public function __construct(public Server $server) {}
|
||||
|
|
|
|||
162
app/Jobs/ServerResourceManager.php
Normal file
162
app/Jobs/ServerResourceManager.php
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ServerResourceManager implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* The time when this job execution started.
|
||||
*/
|
||||
private ?Carbon $executionTime = null;
|
||||
|
||||
private InstanceSettings $settings;
|
||||
|
||||
private string $instanceTimezone;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->onQueue('high');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the middleware the job should pass through.
|
||||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping('server-resource-manager'))
|
||||
->releaseAfter(60),
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Freeze the execution time at the start of the job
|
||||
$this->executionTime = Carbon::now();
|
||||
|
||||
$this->settings = instanceSettings();
|
||||
$this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone');
|
||||
|
||||
if (validate_timezone($this->instanceTimezone) === false) {
|
||||
$this->instanceTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Process server checks - don't let failures stop the job
|
||||
try {
|
||||
$this->processServerChecks();
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Failed to process server checks', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function processServerChecks(): void
|
||||
{
|
||||
$servers = $this->getServers();
|
||||
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
$this->processServer($server);
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing server', [
|
||||
'server_id' => $server->id,
|
||||
'server_name' => $server->name,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getServers()
|
||||
{
|
||||
$allServers = Server::where('ip', '!=', '1.2.3.4');
|
||||
|
||||
if (isCloud()) {
|
||||
$servers = $allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
|
||||
$own = Team::find(0)->servers;
|
||||
|
||||
return $servers->merge($own);
|
||||
} else {
|
||||
return $allServers->get();
|
||||
}
|
||||
}
|
||||
|
||||
private function processServer(Server $server): void
|
||||
{
|
||||
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
|
||||
if (validate_timezone($serverTimezone) === false) {
|
||||
$serverTimezone = config('app.timezone');
|
||||
}
|
||||
|
||||
// Sentinel check
|
||||
$lastSentinelUpdate = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($lastSentinelUpdate)->isBefore($this->executionTime->subSeconds($server->waitBeforeDoingSshCheck()))) {
|
||||
// Dispatch ServerCheckJob if due
|
||||
$checkFrequency = isCloud() ? '*/5 * * * *' : '* * * * *'; // Every 5 min for cloud, every minute for self-hosted
|
||||
if ($this->shouldRunNow($checkFrequency, $serverTimezone)) {
|
||||
ServerCheckJob::dispatch($server);
|
||||
}
|
||||
|
||||
// Dispatch ServerStorageCheckJob if due
|
||||
$serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
|
||||
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
|
||||
}
|
||||
if ($this->shouldRunNow($serverDiskUsageCheckFrequency, $serverTimezone)) {
|
||||
ServerStorageCheckJob::dispatch($server);
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch DockerCleanupJob if due
|
||||
$dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
|
||||
if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) {
|
||||
$dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency];
|
||||
}
|
||||
if ($this->shouldRunNow($dockerCleanupFrequency, $serverTimezone)) {
|
||||
DockerCleanupJob::dispatch($server, false, $server->settings->delete_unused_volumes, $server->settings->delete_unused_networks);
|
||||
}
|
||||
|
||||
// Dispatch ServerPatchCheckJob if due (weekly)
|
||||
if ($this->shouldRunNow('0 0 * * 0', $serverTimezone)) { // Weekly on Sunday at midnight
|
||||
ServerPatchCheckJob::dispatch($server);
|
||||
}
|
||||
|
||||
// Dispatch Sentinel restart if due (daily for Sentinel-enabled servers)
|
||||
if ($server->isSentinelEnabled() && $this->shouldRunNow('0 0 * * *', $serverTimezone)) {
|
||||
dispatch(function () use ($server) {
|
||||
$server->restartContainer('coolify-sentinel');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldRunNow(string $frequency, string $timezone): bool
|
||||
{
|
||||
$cron = new CronExpression($frequency);
|
||||
|
||||
// Use the frozen execution time, not the current time
|
||||
$baseTime = $this->executionTime ?? Carbon::now();
|
||||
$executionTime = $baseTime->copy()->setTimezone($timezone);
|
||||
|
||||
return $cron->isDue($executionTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,9 @@
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Laravel\Horizon\Contracts\Silenced;
|
||||
|
||||
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
|
|
|
|||
|
|
@ -254,10 +254,9 @@ public function submitSmtp()
|
|||
'smtpEncryption.required' => 'Encryption type is required.',
|
||||
]);
|
||||
|
||||
$this->settings->resend_enabled = false;
|
||||
$this->settings->use_instance_email_settings = false;
|
||||
$this->resendEnabled = false;
|
||||
$this->useInstanceEmailSettings = false;
|
||||
if ($this->smtpEnabled) {
|
||||
$this->settings->resend_enabled = $this->resendEnabled = false;
|
||||
}
|
||||
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled;
|
||||
$this->settings->smtp_from_address = $this->smtpFromAddress;
|
||||
|
|
@ -293,11 +292,9 @@ public function submitResend()
|
|||
'smtpFromAddress.email' => 'Please enter a valid email address.',
|
||||
'smtpFromName.required' => 'From Name is required.',
|
||||
]);
|
||||
|
||||
$this->settings->smtp_enabled = false;
|
||||
$this->settings->use_instance_email_settings = false;
|
||||
$this->smtpEnabled = false;
|
||||
$this->useInstanceEmailSettings = false;
|
||||
if ($this->resendEnabled) {
|
||||
$this->settings->smtp_enabled = $this->smtpEnabled = false;
|
||||
}
|
||||
|
||||
$this->settings->resend_enabled = $this->resendEnabled;
|
||||
$this->settings->resend_api_key = $this->resendApiKey;
|
||||
|
|
|
|||
|
|
@ -18,11 +18,13 @@ class Index extends Component
|
|||
|
||||
public int $skip = 0;
|
||||
|
||||
public int $default_take = 10;
|
||||
public int $defaultTake = 10;
|
||||
|
||||
public bool $show_next = false;
|
||||
public bool $showNext = false;
|
||||
|
||||
public bool $show_prev = false;
|
||||
public bool $showPrev = false;
|
||||
|
||||
public int $currentPage = 1;
|
||||
|
||||
public ?string $pull_request_id = null;
|
||||
|
||||
|
|
@ -51,68 +53,111 @@ public function mount()
|
|||
if (! $application) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take);
|
||||
// Validate pull request ID from URL parameters
|
||||
if ($this->pull_request_id !== null && $this->pull_request_id !== '') {
|
||||
if (! is_numeric($this->pull_request_id) || (float) $this->pull_request_id <= 0 || (float) $this->pull_request_id != (int) $this->pull_request_id) {
|
||||
$this->pull_request_id = null;
|
||||
$this->dispatch('error', 'Invalid Pull Request ID in URL. Filter cleared.');
|
||||
} else {
|
||||
// Ensure it's stored as a string representation of a positive integer
|
||||
$this->pull_request_id = (string) (int) $this->pull_request_id;
|
||||
}
|
||||
}
|
||||
|
||||
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->defaultTake, $this->pull_request_id);
|
||||
$this->application = $application;
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
$this->current_url = url()->current();
|
||||
$this->show_pull_request_only();
|
||||
$this->show_more();
|
||||
$this->updateCurrentPage();
|
||||
$this->showMore();
|
||||
}
|
||||
|
||||
private function show_pull_request_only()
|
||||
{
|
||||
if ($this->pull_request_id) {
|
||||
$this->deployments = $this->deployments->where('pull_request_id', $this->pull_request_id);
|
||||
}
|
||||
}
|
||||
|
||||
private function show_more()
|
||||
private function showMore()
|
||||
{
|
||||
if ($this->deployments->count() !== 0) {
|
||||
$this->show_next = true;
|
||||
if ($this->deployments->count() < $this->default_take) {
|
||||
$this->show_next = false;
|
||||
$this->showNext = true;
|
||||
if ($this->deployments->count() < $this->defaultTake) {
|
||||
$this->showNext = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function reload_deployments()
|
||||
public function reloadDeployments()
|
||||
{
|
||||
$this->load_deployments();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function previous_page(?int $take = null)
|
||||
public function previousPage(?int $take = null)
|
||||
{
|
||||
if ($take) {
|
||||
$this->skip = $this->skip - $take;
|
||||
}
|
||||
$this->skip = $this->skip - $this->default_take;
|
||||
$this->skip = $this->skip - $this->defaultTake;
|
||||
if ($this->skip < 0) {
|
||||
$this->show_prev = false;
|
||||
$this->showPrev = false;
|
||||
$this->skip = 0;
|
||||
}
|
||||
$this->load_deployments();
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function next_page(?int $take = null)
|
||||
public function nextPage(?int $take = null)
|
||||
{
|
||||
if ($take) {
|
||||
$this->skip = $this->skip + $take;
|
||||
}
|
||||
$this->show_prev = true;
|
||||
$this->load_deployments();
|
||||
$this->showPrev = true;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function load_deployments()
|
||||
public function loadDeployments()
|
||||
{
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->default_take);
|
||||
['deployments' => $deployments, 'count' => $count] = $this->application->deployments($this->skip, $this->defaultTake, $this->pull_request_id);
|
||||
$this->deployments = $deployments;
|
||||
$this->deployments_count = $count;
|
||||
$this->show_pull_request_only();
|
||||
$this->show_more();
|
||||
$this->showMore();
|
||||
}
|
||||
|
||||
public function updatedPullRequestId($value)
|
||||
{
|
||||
// Sanitize and validate the pull request ID
|
||||
if ($value !== null && $value !== '') {
|
||||
// Check if it's numeric and positive
|
||||
if (! is_numeric($value) || (float) $value <= 0 || (float) $value != (int) $value) {
|
||||
$this->pull_request_id = null;
|
||||
$this->dispatch('error', 'Invalid Pull Request ID. Please enter a valid positive number.');
|
||||
|
||||
return;
|
||||
}
|
||||
// Ensure it's stored as a string representation of a positive integer
|
||||
$this->pull_request_id = (string) (int) $value;
|
||||
} else {
|
||||
$this->pull_request_id = null;
|
||||
}
|
||||
|
||||
// Reset pagination when filter changes
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
public function clearFilter()
|
||||
{
|
||||
$this->pull_request_id = null;
|
||||
$this->skip = 0;
|
||||
$this->showPrev = false;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadDeployments();
|
||||
}
|
||||
|
||||
private function updateCurrentPage()
|
||||
{
|
||||
$this->currentPage = intval($this->skip / $this->defaultTake) + 1;
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Actions\Application\GenerateConfig;
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
|
@ -156,6 +157,14 @@ public function mount()
|
|||
$this->application->settings->save();
|
||||
}
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $serviceName => $domain) {
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$sanitizedDomains[$sanitizedKey] = $domain;
|
||||
}
|
||||
$this->parsedServiceDomains = $sanitizedDomains;
|
||||
|
||||
$this->ports_exposes = $this->application->ports_exposes;
|
||||
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
|
||||
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
|
||||
|
|
@ -206,25 +215,21 @@ public function instantSave()
|
|||
|
||||
}
|
||||
|
||||
public function loadComposeFile($isInit = false)
|
||||
public function loadComposeFile($isInit = false, $showToast = true)
|
||||
{
|
||||
try {
|
||||
if ($isInit && $this->application->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must reload the application to get the latest database changes
|
||||
// Why? Not sure, but it works.
|
||||
// $this->application->refresh();
|
||||
|
||||
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit);
|
||||
if (is_null($this->parsedServices)) {
|
||||
$this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
$showToast && $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->application->parse();
|
||||
$this->dispatch('success', 'Docker compose file loaded.');
|
||||
$showToast && $this->dispatch('success', 'Docker compose file loaded.');
|
||||
$this->dispatch('compose_loaded');
|
||||
$this->dispatch('refreshStorages');
|
||||
$this->dispatch('refreshEnvs');
|
||||
|
|
@ -242,12 +247,31 @@ public function generateDomain(string $serviceName)
|
|||
{
|
||||
$uuid = new Cuid2;
|
||||
$domain = generateFqdn($this->application->destination->server, $uuid);
|
||||
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
$sanitizedKey = str($serviceName)->slug('_')->toString();
|
||||
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
|
||||
|
||||
// Convert back to original service names for storage
|
||||
$originalDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $key => $value) {
|
||||
// Find the original service name by checking parsed services
|
||||
$originalServiceName = $key;
|
||||
if (isset($this->parsedServices['services'])) {
|
||||
foreach ($this->parsedServices['services'] as $originalName => $service) {
|
||||
if (str($originalName)->slug('_')->toString() === $key) {
|
||||
$originalServiceName = $originalName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$originalDomains[$originalServiceName] = $value;
|
||||
}
|
||||
|
||||
$this->application->docker_compose_domains = json_encode($originalDomains);
|
||||
$this->application->save();
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->loadComposeFile();
|
||||
$this->updateServiceEnvironmentVariables();
|
||||
$this->loadComposeFile(showToast: false);
|
||||
}
|
||||
|
||||
return $domain;
|
||||
|
|
@ -429,9 +453,25 @@ public function submit($showToaster = true)
|
|||
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
|
||||
}
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
|
||||
// Convert sanitized service names back to original names for storage
|
||||
$originalDomains = [];
|
||||
foreach ($this->parsedServiceDomains as $key => $value) {
|
||||
// Find the original service name by checking parsed services
|
||||
$originalServiceName = $key;
|
||||
if (isset($this->parsedServices['services'])) {
|
||||
foreach ($this->parsedServices['services'] as $originalName => $service) {
|
||||
if (str($originalName)->slug('_')->toString() === $key) {
|
||||
$originalServiceName = $originalName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$originalDomains[$originalServiceName] = $value;
|
||||
}
|
||||
|
||||
foreach ($this->parsedServiceDomains as $serviceName => $service) {
|
||||
$this->application->docker_compose_domains = json_encode($originalDomains);
|
||||
|
||||
foreach ($originalDomains as $serviceName => $service) {
|
||||
$domain = data_get($service, 'domain');
|
||||
if ($domain) {
|
||||
if (! validate_dns_entry($domain, $this->application->destination->server)) {
|
||||
|
|
@ -446,6 +486,12 @@ public function submit($showToaster = true)
|
|||
}
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
|
||||
// Update SERVICE_FQDN_ and SERVICE_URL_ environment variables for Docker Compose applications
|
||||
if ($this->application->build_pack === 'dockercompose') {
|
||||
$this->updateServiceEnvironmentVariables();
|
||||
}
|
||||
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
|
|
@ -471,4 +517,85 @@ public function downloadConfig()
|
|||
'Content-Disposition' => 'attachment; filename='.$fileName,
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateServiceEnvironmentVariables()
|
||||
{
|
||||
$domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]);
|
||||
|
||||
foreach ($domains as $serviceName => $service) {
|
||||
$serviceNameFormatted = str($serviceName)->upper()->replace('-', '_');
|
||||
$domain = data_get($service, 'domain');
|
||||
|
||||
if ($domain) {
|
||||
// Create or update SERVICE_FQDN_ and SERVICE_URL_ variables
|
||||
$fqdn = Url::fromString($domain);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$fqdnValue = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
$urlValue = str($domain)->after('://');
|
||||
if ($path !== '/') {
|
||||
$urlValue = $urlValue.$path;
|
||||
}
|
||||
|
||||
// Create/update SERVICE_FQDN_
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// Create/update SERVICE_URL_
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// Create/update port-specific variables if port exists
|
||||
if ($port) {
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
|
||||
EnvironmentVariable::where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
EnvironmentVariable::where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Previews extends Component
|
||||
|
|
@ -87,18 +87,9 @@ public function generate_preview($preview_id)
|
|||
return;
|
||||
}
|
||||
|
||||
$fqdn = generateFqdn($this->application->destination->server, $this->application->uuid);
|
||||
$url = Url::fromString($fqdn);
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview->fqdn = $preview_fqdn;
|
||||
$preview->save();
|
||||
$preview->generate_preview_fqdn();
|
||||
$this->application->refresh();
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +119,7 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null)
|
|||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
$this->application->generate_preview_fqdn($pull_request_id);
|
||||
$found->generate_preview_fqdn();
|
||||
$this->application->refresh();
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview added.');
|
||||
|
|
@ -215,48 +206,28 @@ public function stop(int $pull_request_id)
|
|||
public function delete(int $pull_request_id)
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$preview = ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first();
|
||||
|
||||
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);
|
||||
if (! $preview) {
|
||||
$this->dispatch('error', 'Preview not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first()
|
||||
->delete();
|
||||
// Soft delete immediately for instant UI feedback
|
||||
$preview->delete();
|
||||
|
||||
$this->application->refresh();
|
||||
// Dispatch the job for async cleanup (container stopping + force delete)
|
||||
DeleteResourceJob::dispatch($preview);
|
||||
|
||||
// Refresh the application and its previews relationship to reflect the soft delete
|
||||
$this->application->load('previews');
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Preview deleted.');
|
||||
$this->dispatch('success', 'Preview deletion started. It may take a few moments to complete.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
private function stopContainers(array $containers, $server, int $timeout = 30)
|
||||
{
|
||||
if (empty($containers)) {
|
||||
return;
|
||||
}
|
||||
$containerNames = [];
|
||||
foreach ($containers as $container) {
|
||||
$containerNames[] = str_replace('/', '', $container['Names']);
|
||||
}
|
||||
|
||||
$containerList = implode(' ', array_map('escapeshellarg', $containerNames));
|
||||
$commands = [
|
||||
"docker stop --time=$timeout $containerList",
|
||||
"docker rm -f $containerList",
|
||||
];
|
||||
|
||||
instant_remote_process(
|
||||
command: $commands,
|
||||
server: $server,
|
||||
throwError: false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,9 +38,25 @@ public function generate()
|
|||
$domain = $domains->first(function ($_, $key) {
|
||||
return $key === $this->serviceName;
|
||||
});
|
||||
if ($domain) {
|
||||
$domain = data_get($domain, 'domain');
|
||||
$url = Url::fromString($domain);
|
||||
|
||||
$domain_string = data_get($domain, 'domain');
|
||||
|
||||
// If no domain is set in the main application, generate a default domain
|
||||
if (empty($domain_string)) {
|
||||
$server = $this->preview->application->destination->server;
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$random = new Cuid2;
|
||||
|
||||
// Generate a unique domain like main app services do
|
||||
$generated_fqdn = generateFqdn($server, $random);
|
||||
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', str($generated_fqdn)->after('://'), $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = str($generated_fqdn)->before('://').'://'.$preview_fqdn;
|
||||
} else {
|
||||
// Use the existing domain from the main application
|
||||
$url = Url::fromString($domain_string);
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
|
|
@ -49,12 +65,15 @@ public function generate()
|
|||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->preview->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
|
||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->preview->save();
|
||||
}
|
||||
|
||||
// Save the generated domain
|
||||
$docker_compose_domains = data_get($this->preview, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
||||
$docker_compose_domains[$this->serviceName]['domain'] = $this->service->domain = $preview_fqdn;
|
||||
$this->preview->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->preview->save();
|
||||
|
||||
$this->dispatch('update_links');
|
||||
$this->dispatch('success', 'Domain generated.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,8 +111,19 @@ public function changeSource($sourceId, $sourceType)
|
|||
$this->application->update([
|
||||
'source_id' => $sourceId,
|
||||
'source_type' => $sourceType,
|
||||
'repository_project_id' => null,
|
||||
]);
|
||||
|
||||
['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!');
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ public function mount($project_uuid)
|
|||
$this->project_id = $this->project->id;
|
||||
$this->servers = currentTeam()
|
||||
->servers()
|
||||
->with('destinations')
|
||||
->get()
|
||||
->reject(fn ($server) => $server->isBuildServer());
|
||||
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
|
||||
|
|
@ -455,7 +454,7 @@ public function clone(string $type)
|
|||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($application, false, false);
|
||||
StopService::dispatch($application);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $application->service->destination->server;
|
||||
|
|
@ -509,7 +508,7 @@ public function clone(string $type)
|
|||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($database->service, false, false);
|
||||
StopService::dispatch($database->service);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $database->service->destination->server;
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ class BackupEdit extends Component
|
|||
#[Validate(['required', 'boolean'])]
|
||||
public bool $dumpAll = false;
|
||||
|
||||
#[Validate(['required', 'int', 'min:1', 'max:36000'])]
|
||||
public int $timeout = 3600;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
|
|
@ -98,6 +101,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->backup->s3_storage_id = $this->s3StorageId;
|
||||
$this->backup->databases_to_backup = $this->databasesToBackup;
|
||||
$this->backup->dump_all = $this->dumpAll;
|
||||
$this->backup->timeout = $this->timeout;
|
||||
$this->customValidate();
|
||||
$this->backup->save();
|
||||
} else {
|
||||
|
|
@ -114,6 +118,7 @@ public function syncData(bool $toModel = false)
|
|||
$this->s3StorageId = $this->backup->s3_storage_id;
|
||||
$this->databasesToBackup = $this->backup->databases_to_backup;
|
||||
$this->dumpAll = $this->backup->dump_all;
|
||||
$this->timeout = $this->backup->timeout;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
|
@ -14,7 +15,19 @@ class BackupExecutions extends Component
|
|||
|
||||
public $database;
|
||||
|
||||
public $executions = [];
|
||||
public ?Collection $executions;
|
||||
|
||||
public int $executions_count = 0;
|
||||
|
||||
public int $skip = 0;
|
||||
|
||||
public int $defaultTake = 10;
|
||||
|
||||
public bool $showNext = false;
|
||||
|
||||
public bool $showPrev = false;
|
||||
|
||||
public int $currentPage = 1;
|
||||
|
||||
public $setDeletableBackup;
|
||||
|
||||
|
|
@ -40,6 +53,20 @@ public function cleanupFailed()
|
|||
}
|
||||
}
|
||||
|
||||
public function cleanupDeleted()
|
||||
{
|
||||
if ($this->backup) {
|
||||
$deletedCount = $this->backup->executions()->where('local_storage_deleted', true)->count();
|
||||
if ($deletedCount > 0) {
|
||||
$this->backup->executions()->where('local_storage_deleted', true)->delete();
|
||||
$this->refreshBackupExecutions();
|
||||
$this->dispatch('success', "Cleaned up {$deletedCount} backup entries deleted from local storage.");
|
||||
} else {
|
||||
$this->dispatch('info', 'No backup entries found that are deleted from local storage.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
|
|
@ -85,18 +112,74 @@ public function download_file($exeuctionId)
|
|||
|
||||
public function refreshBackupExecutions(): void
|
||||
{
|
||||
if ($this->backup && $this->backup->exists) {
|
||||
$this->executions = $this->backup->executions()->get()->toArray();
|
||||
} else {
|
||||
$this->executions = [];
|
||||
$this->loadExecutions();
|
||||
}
|
||||
|
||||
public function reloadExecutions()
|
||||
{
|
||||
$this->loadExecutions();
|
||||
}
|
||||
|
||||
public function previousPage(?int $take = null)
|
||||
{
|
||||
if ($take) {
|
||||
$this->skip = $this->skip - $take;
|
||||
}
|
||||
$this->skip = $this->skip - $this->defaultTake;
|
||||
if ($this->skip < 0) {
|
||||
$this->showPrev = false;
|
||||
$this->skip = 0;
|
||||
}
|
||||
$this->updateCurrentPage();
|
||||
$this->loadExecutions();
|
||||
}
|
||||
|
||||
public function nextPage(?int $take = null)
|
||||
{
|
||||
if ($take) {
|
||||
$this->skip = $this->skip + $take;
|
||||
}
|
||||
$this->showPrev = true;
|
||||
$this->updateCurrentPage();
|
||||
$this->loadExecutions();
|
||||
}
|
||||
|
||||
private function loadExecutions()
|
||||
{
|
||||
if ($this->backup && $this->backup->exists) {
|
||||
['executions' => $executions, 'count' => $count] = $this->backup->executionsPaginated($this->skip, $this->defaultTake);
|
||||
$this->executions = $executions;
|
||||
$this->executions_count = $count;
|
||||
} else {
|
||||
$this->executions = collect([]);
|
||||
$this->executions_count = 0;
|
||||
}
|
||||
$this->showMore();
|
||||
}
|
||||
|
||||
private function showMore()
|
||||
{
|
||||
if ($this->executions->count() !== 0) {
|
||||
$this->showNext = true;
|
||||
if ($this->executions->count() < $this->defaultTake) {
|
||||
$this->showNext = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function updateCurrentPage()
|
||||
{
|
||||
$this->currentPage = intval($this->skip / $this->defaultTake) + 1;
|
||||
}
|
||||
|
||||
public function mount(ScheduledDatabaseBackup $backup)
|
||||
{
|
||||
$this->backup = $backup;
|
||||
$this->database = $backup->database;
|
||||
$this->refreshBackupExecutions();
|
||||
$this->updateCurrentPage();
|
||||
$this->loadExecutions();
|
||||
}
|
||||
|
||||
public function server()
|
||||
|
|
@ -121,8 +204,8 @@ public function render()
|
|||
{
|
||||
return view('livewire.project.database.backup-executions', [
|
||||
'checkboxes' => [
|
||||
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
|
||||
// ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
|
||||
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently from S3 Storage'],
|
||||
// ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently from SFTP Storage'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Actions\Service\StartService;
|
||||
use App\Actions\Service\StopService;
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
|
@ -64,7 +63,7 @@ public function serviceChecked()
|
|||
$this->service->databases->each(function ($database) {
|
||||
$database->refresh();
|
||||
});
|
||||
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
|
||||
if (is_null($this->service->config_hash)) {
|
||||
$this->service->isConfigurationChanged(true);
|
||||
}
|
||||
$this->dispatch('configurationChanged');
|
||||
|
|
@ -96,7 +95,7 @@ public function checkDeployments()
|
|||
public function start()
|
||||
{
|
||||
$activity = StartService::run($this->service, pullLatestImages: true);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function forceDeploy()
|
||||
|
|
@ -112,7 +111,7 @@ public function forceDeploy()
|
|||
$activity->save();
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
|
|
@ -136,7 +135,7 @@ public function restart()
|
|||
return;
|
||||
}
|
||||
$activity = StartService::run($this->service, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function pullAndRestartEvent()
|
||||
|
|
@ -148,7 +147,7 @@ public function pullAndRestartEvent()
|
|||
return;
|
||||
}
|
||||
$activity = StartService::run($this->service, pullLatestImages: true, stopBeforeStart: true);
|
||||
$this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class);
|
||||
$this->dispatch('activityMonitor', $activity->id);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -99,10 +99,10 @@ public function delete($password)
|
|||
$this->resource->delete();
|
||||
DeleteResourceJob::dispatch(
|
||||
$this->resource,
|
||||
$this->delete_configurations,
|
||||
$this->delete_volumes,
|
||||
$this->docker_cleanup,
|
||||
$this->delete_connected_networks
|
||||
$this->delete_connected_networks,
|
||||
$this->delete_configurations,
|
||||
$this->docker_cleanup
|
||||
);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@ public function submit()
|
|||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Environment variable updated.');
|
||||
$this->dispatch('envsUpdated');
|
||||
$this->dispatch('configurationChanged');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,13 @@ public function loadContainers()
|
|||
}
|
||||
}
|
||||
|
||||
public function updatedSelectedContainer()
|
||||
{
|
||||
if ($this->selected_container !== 'default') {
|
||||
$this->connectToContainer();
|
||||
}
|
||||
}
|
||||
|
||||
#[On('connectToServer')]
|
||||
public function connectToServer()
|
||||
{
|
||||
|
|
@ -151,6 +158,9 @@ public function connectToServer()
|
|||
data_get($server, 'name'),
|
||||
data_get($server, 'uuid')
|
||||
);
|
||||
|
||||
// Dispatch a frontend event to ensure terminal gets focus after connection
|
||||
$this->dispatch('terminal-should-focus');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
|
@ -206,6 +216,9 @@ public function connectToContainer()
|
|||
data_get($container, 'container.Names'),
|
||||
data_get($container, 'server.uuid')
|
||||
);
|
||||
|
||||
// Dispatch a frontend event to ensure terminal gets focus after connection
|
||||
$this->dispatch('terminal-should-focus');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ public function cloneTo($destination_id)
|
|||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($application, false, false);
|
||||
StopService::dispatch($application);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $application->service->destination->server;
|
||||
|
|
@ -454,7 +454,7 @@ public function cloneTo($destination_id)
|
|||
|
||||
if ($this->cloneVolumeData) {
|
||||
try {
|
||||
StopService::dispatch($database->service, false, false);
|
||||
StopService::dispatch($database->service);
|
||||
$sourceVolume = $volume->name;
|
||||
$targetVolume = $newPersistentVolume->name;
|
||||
$sourceServer = $database->service->destination->server;
|
||||
|
|
|
|||
|
|
@ -68,11 +68,16 @@ public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
|||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ public function instantSave()
|
|||
public function manualCleanup()
|
||||
{
|
||||
try {
|
||||
DockerCleanupJob::dispatch($this->server, true);
|
||||
DockerCleanupJob::dispatch($this->server, true, $this->deleteUnusedVolumes, $this->deleteUnusedNetworks);
|
||||
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,15 @@ class Proxy extends Component
|
|||
|
||||
public ?string $redirect_url = null;
|
||||
|
||||
protected $listeners = ['saveConfiguration' => 'submit'];
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
'saveConfiguration' => 'submit',
|
||||
"echo-private:team.{$teamId},ProxyStatusChangedUI" => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.generate_exact_labels' => 'required|boolean',
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ public function updateAllPackages()
|
|||
{
|
||||
if (! $this->packageManager || ! $this->osId) {
|
||||
$this->dispatch('error', message: 'Run “Check for updates” first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
118
app/Livewire/Settings/Advanced.php
Normal file
118
app/Livewire/Settings/Advanced.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Auth;
|
||||
use Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Advanced extends Component
|
||||
{
|
||||
#[Validate('required')]
|
||||
public Server $server;
|
||||
|
||||
public InstanceSettings $settings;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_registration_enabled;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $do_not_track;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_dns_validation_enabled;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $custom_dns_servers = null;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_api_enabled;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $allowed_ips = null;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_sponsorship_popup_enabled;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->settings = instanceSettings();
|
||||
$this->custom_dns_servers = $this->settings->custom_dns_servers;
|
||||
$this->allowed_ips = $this->settings->allowed_ips;
|
||||
$this->do_not_track = $this->settings->do_not_track;
|
||||
$this->is_registration_enabled = $this->settings->is_registration_enabled;
|
||||
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
|
||||
$this->is_api_enabled = $this->settings->is_api_enabled;
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
$this->is_sponsorship_popup_enabled = $this->settings->is_sponsorship_popup_enabled;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
|
||||
$this->custom_dns_servers = str($this->custom_dns_servers)->replaceEnd(',', '')->trim();
|
||||
$this->custom_dns_servers = str($this->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
|
||||
return str($dns)->trim()->lower();
|
||||
})->unique()->implode(',');
|
||||
|
||||
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
|
||||
$this->allowed_ips = str($this->allowed_ips)->trim()->explode(',')->map(function ($ip) {
|
||||
return str($ip)->trim();
|
||||
})->unique()->implode(',');
|
||||
|
||||
$this->instantSave();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->settings->is_registration_enabled = $this->is_registration_enabled;
|
||||
$this->settings->do_not_track = $this->do_not_track;
|
||||
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
|
||||
$this->settings->custom_dns_servers = $this->custom_dns_servers;
|
||||
$this->settings->is_api_enabled = $this->is_api_enabled;
|
||||
$this->settings->allowed_ips = $this->allowed_ips;
|
||||
$this->settings->is_sponsorship_popup_enabled = $this->is_sponsorship_popup_enabled;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTwoStepConfirmation($password): bool
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Two step confirmation has been disabled.');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.advanced');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
|
@ -15,10 +12,7 @@ class Index extends Component
|
|||
{
|
||||
public InstanceSettings $settings;
|
||||
|
||||
protected Server $server;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_auto_update_enabled;
|
||||
public Server $server;
|
||||
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public ?string $fqdn = null;
|
||||
|
|
@ -29,45 +23,18 @@ class Index extends Component
|
|||
#[Validate('required|integer|min:1025|max:65535')]
|
||||
public int $public_port_max;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $custom_dns_servers = null;
|
||||
|
||||
#[Validate('nullable|string|max:255')]
|
||||
public ?string $instance_name = null;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $allowed_ips = null;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $public_ipv4 = null;
|
||||
|
||||
#[Validate('nullable|string')]
|
||||
public ?string $public_ipv6 = null;
|
||||
|
||||
#[Validate('string')]
|
||||
public string $auto_update_frequency;
|
||||
|
||||
#[Validate('string|required')]
|
||||
public string $update_check_frequency;
|
||||
|
||||
#[Validate('required|string|timezone')]
|
||||
public string $instance_timezone;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $do_not_track;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_registration_enabled;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_dns_validation_enabled;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_api_enabled;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.index');
|
||||
|
|
@ -77,26 +44,16 @@ public function mount()
|
|||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return redirect()->route('dashboard');
|
||||
} else {
|
||||
$this->settings = instanceSettings();
|
||||
$this->fqdn = $this->settings->fqdn;
|
||||
$this->public_port_min = $this->settings->public_port_min;
|
||||
$this->public_port_max = $this->settings->public_port_max;
|
||||
$this->custom_dns_servers = $this->settings->custom_dns_servers;
|
||||
$this->instance_name = $this->settings->instance_name;
|
||||
$this->allowed_ips = $this->settings->allowed_ips;
|
||||
$this->public_ipv4 = $this->settings->public_ipv4;
|
||||
$this->public_ipv6 = $this->settings->public_ipv6;
|
||||
$this->do_not_track = $this->settings->do_not_track;
|
||||
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
||||
$this->is_registration_enabled = $this->settings->is_registration_enabled;
|
||||
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
|
||||
$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->instance_timezone = $this->settings->instance_timezone;
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
}
|
||||
$this->settings = instanceSettings();
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->fqdn = $this->settings->fqdn;
|
||||
$this->public_port_min = $this->settings->public_port_min;
|
||||
$this->public_port_max = $this->settings->public_port_max;
|
||||
$this->instance_name = $this->settings->instance_name;
|
||||
$this->public_ipv4 = $this->settings->public_ipv4;
|
||||
$this->public_ipv6 = $this->settings->public_ipv6;
|
||||
$this->instance_timezone = $this->settings->instance_timezone;
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
|
|
@ -111,28 +68,12 @@ public function timezones(): array
|
|||
public function instantSave($isSave = true)
|
||||
{
|
||||
$this->validate();
|
||||
if ($this->settings->is_auto_update_enabled === true) {
|
||||
$this->validate([
|
||||
'auto_update_frequency' => ['required', 'string'],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->settings->fqdn = $this->fqdn;
|
||||
$this->settings->public_port_min = $this->public_port_min;
|
||||
$this->settings->public_port_max = $this->public_port_max;
|
||||
$this->settings->custom_dns_servers = $this->custom_dns_servers;
|
||||
$this->settings->instance_name = $this->instance_name;
|
||||
$this->settings->allowed_ips = $this->allowed_ips;
|
||||
$this->settings->public_ipv4 = $this->public_ipv4;
|
||||
$this->settings->public_ipv6 = $this->public_ipv6;
|
||||
$this->settings->do_not_track = $this->do_not_track;
|
||||
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
|
||||
$this->settings->is_registration_enabled = $this->is_registration_enabled;
|
||||
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
|
||||
$this->settings->is_api_enabled = $this->is_api_enabled;
|
||||
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
||||
$this->settings->update_check_frequency = $this->update_check_frequency;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->instance_timezone = $this->instance_timezone;
|
||||
if ($isSave) {
|
||||
$this->settings->save();
|
||||
|
|
@ -144,7 +85,6 @@ public function submit()
|
|||
{
|
||||
try {
|
||||
$error_show = false;
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->resetErrorBag();
|
||||
|
||||
if (! validate_timezone($this->instance_timezone)) {
|
||||
|
|
@ -161,46 +101,15 @@ public function submit()
|
|||
}
|
||||
$this->validate();
|
||||
|
||||
if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) {
|
||||
$this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.');
|
||||
if (empty($this->auto_update_frequency)) {
|
||||
$this->auto_update_frequency = '0 0 * * *';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! validate_cron_expression($this->update_check_frequency)) {
|
||||
$this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.');
|
||||
if (empty($this->update_check_frequency)) {
|
||||
$this->update_check_frequency = '0 * * * *';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->settings->is_dns_validation_enabled && $this->settings->fqdn) {
|
||||
if (! validate_dns_entry($this->settings->fqdn, $this->server)) {
|
||||
$this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->settings->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
if ($this->settings->is_dns_validation_enabled && $this->fqdn) {
|
||||
if (! validate_dns_entry($this->fqdn, $this->server)) {
|
||||
$this->dispatch('error', "Validating DNS failed.<br><br>Make sure you have added the DNS records correctly.<br><br>{$this->fqdn}->{$this->server->ip}<br><br>Check this <a target='_blank' class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/dns-configuration'>documentation</a> for further help.");
|
||||
$error_show = true;
|
||||
}
|
||||
}
|
||||
if ($this->settings->fqdn) {
|
||||
check_domain_usage(domain: $this->settings->fqdn);
|
||||
if ($this->fqdn) {
|
||||
check_domain_usage(domain: $this->fqdn);
|
||||
}
|
||||
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->replaceEnd(',', '')->trim();
|
||||
$this->settings->custom_dns_servers = str($this->settings->custom_dns_servers)->trim()->explode(',')->map(function ($dns) {
|
||||
return str($dns)->trim()->lower();
|
||||
});
|
||||
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique();
|
||||
$this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(',');
|
||||
|
||||
$this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim();
|
||||
$this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) {
|
||||
return str($ip)->trim();
|
||||
});
|
||||
$this->settings->allowed_ips = $this->settings->allowed_ips->unique();
|
||||
$this->settings->allowed_ips = $this->settings->allowed_ips->implode(',');
|
||||
|
||||
$this->instantSave(isSave: false);
|
||||
|
||||
|
|
@ -213,31 +122,4 @@ public function submit()
|
|||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkManually()
|
||||
{
|
||||
CheckForUpdatesJob::dispatchSync();
|
||||
$this->dispatch('updateAvailable');
|
||||
$settings = instanceSettings();
|
||||
if ($settings->new_version_available) {
|
||||
$this->dispatch('success', 'New version available!');
|
||||
} else {
|
||||
$this->dispatch('success', 'No new version available.');
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleTwoStepConfirmation($password): bool
|
||||
{
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Two step confirmation has been disabled.');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
101
app/Livewire/Settings/Updates.php
Normal file
101
app/Livewire/Settings/Updates.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Updates extends Component
|
||||
{
|
||||
public InstanceSettings $settings;
|
||||
|
||||
public Server $server;
|
||||
|
||||
#[Validate('string')]
|
||||
public string $auto_update_frequency;
|
||||
|
||||
#[Validate('string|required')]
|
||||
public string $update_check_frequency;
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $is_auto_update_enabled;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->server = Server::findOrFail(0);
|
||||
|
||||
$this->settings = instanceSettings();
|
||||
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
||||
$this->update_check_frequency = $this->settings->update_check_frequency;
|
||||
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
if ($this->settings->is_auto_update_enabled === true) {
|
||||
$this->validate([
|
||||
'auto_update_frequency' => ['required', 'string'],
|
||||
]);
|
||||
}
|
||||
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
||||
$this->settings->update_check_frequency = $this->update_check_frequency;
|
||||
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->resetErrorBag();
|
||||
$this->validate();
|
||||
|
||||
if ($this->is_auto_update_enabled && ! validate_cron_expression($this->auto_update_frequency)) {
|
||||
$this->dispatch('error', 'Invalid Cron / Human expression for Auto Update Frequency.');
|
||||
if (empty($this->auto_update_frequency)) {
|
||||
$this->auto_update_frequency = '0 0 * * *';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (! validate_cron_expression($this->update_check_frequency)) {
|
||||
$this->dispatch('error', 'Invalid Cron / Human expression for Update Check Frequency.');
|
||||
if (empty($this->update_check_frequency)) {
|
||||
$this->update_check_frequency = '0 * * * *';
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->instantSave();
|
||||
$this->server->setupDynamicProxyConfiguration();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkManually()
|
||||
{
|
||||
CheckForUpdatesJob::dispatchSync();
|
||||
$this->dispatch('updateAvailable');
|
||||
$settings = instanceSettings();
|
||||
if ($settings->new_version_available) {
|
||||
$this->dispatch('success', 'New version available!');
|
||||
} else {
|
||||
$this->dispatch('success', 'No new version available.');
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.updates');
|
||||
}
|
||||
}
|
||||
|
|
@ -46,32 +46,31 @@ public function mount()
|
|||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return redirect()->route('dashboard');
|
||||
} else {
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
|
||||
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
|
||||
if ($this->database) {
|
||||
$this->uuid = $this->database->uuid;
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->postgres_user = $this->database->postgres_user;
|
||||
$this->postgres_password = $this->database->postgres_password;
|
||||
|
||||
if ($this->database->status !== 'running') {
|
||||
$this->database->status = 'running';
|
||||
$this->database->save();
|
||||
}
|
||||
$this->backup = $this->database->scheduledBackups->first();
|
||||
if ($this->backup && ! $this->server->isFunctional()) {
|
||||
$this->backup->enabled = false;
|
||||
$this->backup->save();
|
||||
}
|
||||
$this->executions = $this->backup->executions;
|
||||
}
|
||||
$this->settings = $settings;
|
||||
$this->s3s = $s3s;
|
||||
}
|
||||
$settings = instanceSettings();
|
||||
$this->server = Server::findOrFail(0);
|
||||
$this->database = StandalonePostgresql::whereName('coolify-db')->first();
|
||||
$s3s = S3Storage::whereTeamId(0)->get() ?? [];
|
||||
if ($this->database) {
|
||||
$this->uuid = $this->database->uuid;
|
||||
$this->name = $this->database->name;
|
||||
$this->description = $this->database->description;
|
||||
$this->postgres_user = $this->database->postgres_user;
|
||||
$this->postgres_password = $this->database->postgres_password;
|
||||
|
||||
if ($this->database->status !== 'running') {
|
||||
$this->database->status = 'running';
|
||||
$this->database->save();
|
||||
}
|
||||
$this->backup = $this->database->scheduledBackups->first();
|
||||
if ($this->backup && ! $this->server->isFunctional()) {
|
||||
$this->backup->enabled = false;
|
||||
$this->backup->save();
|
||||
}
|
||||
$this->executions = $this->backup->executions;
|
||||
}
|
||||
$this->settings = $settings;
|
||||
$this->s3s = $s3s;
|
||||
}
|
||||
|
||||
public function addCoolifyDatabase()
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class Form extends Component
|
|||
'storage.endpoint' => 'Endpoint',
|
||||
];
|
||||
|
||||
public function test_s3_connection()
|
||||
public function testConnection()
|
||||
{
|
||||
try {
|
||||
$this->storage->testConnection(shouldSave: true);
|
||||
|
|
@ -45,6 +45,8 @@ public function test_s3_connection()
|
|||
public function delete()
|
||||
{
|
||||
try {
|
||||
$this->authorize('delete', $this->storage);
|
||||
|
||||
$this->storage->delete();
|
||||
|
||||
return redirect()->route('storage.index');
|
||||
|
|
@ -57,7 +59,7 @@ public function submit()
|
|||
{
|
||||
$this->validate();
|
||||
try {
|
||||
$this->test_s3_connection();
|
||||
$this->testConnection();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
|
@ -53,30 +52,12 @@ public function getUsers()
|
|||
}
|
||||
}
|
||||
|
||||
private function finalizeDeletion(User $user, Team $team)
|
||||
{
|
||||
$servers = $team->servers;
|
||||
foreach ($servers as $server) {
|
||||
$resources = $server->definedResources();
|
||||
foreach ($resources as $resource) {
|
||||
$resource->forceDelete();
|
||||
}
|
||||
$server->forceDelete();
|
||||
}
|
||||
|
||||
$projects = $team->projects;
|
||||
foreach ($projects as $project) {
|
||||
$project->forceDelete();
|
||||
}
|
||||
$team->members()->detach($user->id);
|
||||
$team->delete();
|
||||
}
|
||||
|
||||
public function delete($id, $password)
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
|
@ -84,52 +65,22 @@ public function delete($id, $password)
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (! auth()->user()->isInstanceAdmin()) {
|
||||
return $this->dispatch('error', 'You are not authorized to delete users');
|
||||
}
|
||||
|
||||
$user = User::find($id);
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$user_alone_in_team = $team->members->count() === 1;
|
||||
if ($team->id === 0) {
|
||||
if ($user_alone_in_team) {
|
||||
return $this->dispatch('error', 'User is alone in the root team, cannot delete');
|
||||
}
|
||||
}
|
||||
if ($user_alone_in_team) {
|
||||
$this->finalizeDeletion($user, $team);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($user->isOwner()) {
|
||||
$found_other_owner_or_admin = $team->members->filter(function ($member) {
|
||||
return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
|
||||
})->where('id', '!=', $user->id)->first();
|
||||
|
||||
if ($found_other_owner_or_admin) {
|
||||
$team->members()->detach($user->id);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$found_other_member_who_is_not_owner = $team->members->filter(function ($member) {
|
||||
return $member->pivot->role === 'member';
|
||||
})->first();
|
||||
if ($found_other_member_who_is_not_owner) {
|
||||
$found_other_member_who_is_not_owner->pivot->role = 'owner';
|
||||
$found_other_member_who_is_not_owner->pivot->save();
|
||||
$team->members()->detach($user->id);
|
||||
} else {
|
||||
$this->finalizeDeletion($user, $team);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$team->members()->detach($user->id);
|
||||
}
|
||||
if (! $user) {
|
||||
return $this->dispatch('error', 'User not found');
|
||||
}
|
||||
|
||||
try {
|
||||
$user->delete();
|
||||
$this->getUsers();
|
||||
} catch (\Exception $e) {
|
||||
return $this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
$user->delete();
|
||||
$this->getUsers();
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
|
||||
class Invitations extends Component
|
||||
|
|
@ -14,8 +15,13 @@ class Invitations extends Component
|
|||
public function deleteInvitation(int $invitation_id)
|
||||
{
|
||||
try {
|
||||
$initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
|
||||
$initiation_found->delete();
|
||||
$invitation = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id);
|
||||
$user = User::whereEmail($invitation->email)->first();
|
||||
if (filled($user)) {
|
||||
$user->deleteIfNotVerifiedAndForcePasswordReset();
|
||||
}
|
||||
|
||||
$invitation->delete();
|
||||
$this->refreshInvitations();
|
||||
$this->dispatch('success', 'Invitation revoked.');
|
||||
} catch (\Exception) {
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ public function mount()
|
|||
|
||||
public function viaEmail()
|
||||
{
|
||||
$this->generate_invite_link(sendEmail: true);
|
||||
$this->generateInviteLink(sendEmail: true);
|
||||
}
|
||||
|
||||
public function viaLink()
|
||||
{
|
||||
$this->generate_invite_link(sendEmail: false);
|
||||
$this->generateInviteLink(sendEmail: false);
|
||||
}
|
||||
|
||||
private function generate_invite_link(bool $sendEmail = false)
|
||||
private function generateInviteLink(bool $sendEmail = false)
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
|
|
|
|||
|
|
@ -798,7 +798,7 @@ public function environment()
|
|||
|
||||
public function previews()
|
||||
{
|
||||
return $this->hasMany(ApplicationPreview::class);
|
||||
return $this->hasMany(ApplicationPreview::class)->orderBy('pull_request_id', 'desc');
|
||||
}
|
||||
|
||||
public function deployment_queue()
|
||||
|
|
@ -836,9 +836,14 @@ public function get_last_days_deployments()
|
|||
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
|
||||
}
|
||||
|
||||
public function deployments(int $skip = 0, int $take = 10)
|
||||
public function deployments(int $skip = 0, int $take = 10, ?string $pullRequestId = null)
|
||||
{
|
||||
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc');
|
||||
|
||||
if ($pullRequestId) {
|
||||
$deployments = $deployments->where('pull_request_id', $pullRequestId);
|
||||
}
|
||||
|
||||
$count = $deployments->count();
|
||||
$deployments = $deployments->skip($skip)->take($take)->get();
|
||||
|
||||
|
|
@ -1578,34 +1583,6 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
|
|||
}
|
||||
}
|
||||
|
||||
public function generate_preview_fqdn(int $pull_request_id)
|
||||
{
|
||||
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->id, $pull_request_id);
|
||||
if (is_null(data_get($preview, 'fqdn')) && $this->fqdn) {
|
||||
if (str($this->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->fqdn)->explode(',')[0]);
|
||||
} else {
|
||||
$url = Url::fromString($this->fqdn);
|
||||
if (data_get($preview, 'fqdn')) {
|
||||
$preview_fqdn = getFqdnWithoutPort(data_get($preview, 'fqdn'));
|
||||
}
|
||||
}
|
||||
$template = $this->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview->fqdn = $preview_fqdn;
|
||||
$preview->save();
|
||||
}
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
public static function getDomainsByUuid(string $uuid): array
|
||||
{
|
||||
$application = self::where('uuid', $uuid)->first();
|
||||
|
|
|
|||
|
|
@ -2,19 +2,25 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ApplicationPreview extends BaseModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::deleting(function ($preview) {
|
||||
static::forceDeleting(function ($preview) {
|
||||
$server = $preview->application->destination->server;
|
||||
$application = $preview->application;
|
||||
|
||||
if (data_get($preview, 'application.build_pack') === 'dockercompose') {
|
||||
$server = $preview->application->destination->server;
|
||||
$composeFile = $preview->application->parse(pull_request_id: $preview->pull_request_id);
|
||||
// Docker Compose volume and network cleanup
|
||||
$composeFile = $application->parse(pull_request_id: $preview->pull_request_id);
|
||||
$volumes = data_get($composeFile, 'volumes');
|
||||
$networks = data_get($composeFile, 'networks');
|
||||
$networkKeys = collect($networks)->keys();
|
||||
|
|
@ -26,7 +32,18 @@ protected static function booted()
|
|||
instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm $key"], $server, false);
|
||||
});
|
||||
} else {
|
||||
// Regular application volume cleanup
|
||||
$persistentStorages = $preview->persistentStorages()->get() ?? collect();
|
||||
if ($persistentStorages->count() > 0) {
|
||||
foreach ($persistentStorages as $storage) {
|
||||
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up persistent storage records
|
||||
$preview->persistentStorages()->delete();
|
||||
});
|
||||
static::saving(function ($preview) {
|
||||
if ($preview->isDirty('status')) {
|
||||
|
|
@ -50,12 +67,23 @@ public function application()
|
|||
return $this->belongsTo(Application::class);
|
||||
}
|
||||
|
||||
public function generate_preview_fqdn_compose()
|
||||
public function persistentStorages()
|
||||
{
|
||||
$domains = collect(json_decode($this->application->docker_compose_domains)) ?? collect();
|
||||
foreach ($domains as $service_name => $domain) {
|
||||
$domain = data_get($domain, 'domain');
|
||||
$url = Url::fromString($domain);
|
||||
return $this->morphMany(\App\Models\LocalPersistentVolume::class, 'resource');
|
||||
}
|
||||
|
||||
public function generate_preview_fqdn()
|
||||
{
|
||||
if (is_null($this->fqdn) && $this->application->fqdn) {
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
|
||||
} else {
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
if ($this->fqdn) {
|
||||
$preview_fqdn = getFqdnWithoutPort($this->fqdn);
|
||||
}
|
||||
}
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
|
|
@ -64,12 +92,76 @@ public function generate_preview_fqdn_compose()
|
|||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$docker_compose_domains = data_get($this, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true);
|
||||
$docker_compose_domains[$service_name]['domain'] = $preview_fqdn;
|
||||
$docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->docker_compose_domains = $docker_compose_domains;
|
||||
$this->fqdn = $preview_fqdn;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function generate_preview_fqdn_compose()
|
||||
{
|
||||
$services = collect(json_decode($this->application->docker_compose_domains)) ?? collect();
|
||||
$docker_compose_domains = data_get($this, 'docker_compose_domains');
|
||||
$docker_compose_domains = json_decode($docker_compose_domains, true) ?? [];
|
||||
|
||||
// Get all services from the parsed compose file to ensure all services have entries
|
||||
$parsedServices = $this->application->parse(pull_request_id: $this->pull_request_id);
|
||||
if (isset($parsedServices['services'])) {
|
||||
foreach ($parsedServices['services'] as $serviceName => $service) {
|
||||
if (! isDatabaseImage(data_get($service, 'image'))) {
|
||||
// Remove PR suffix from service name to get original service name
|
||||
$originalServiceName = str($serviceName)->replaceLast('-pr-'.$this->pull_request_id, '')->toString();
|
||||
|
||||
// Ensure all services have an entry, even if empty
|
||||
if (! $services->has($originalServiceName)) {
|
||||
$services->put($originalServiceName, ['domain' => '']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($services as $service_name => $service_config) {
|
||||
$domain_string = data_get($service_config, 'domain');
|
||||
|
||||
// If domain string is empty or null, don't auto-generate domain
|
||||
// Only generate domains when main app already has domains set
|
||||
if (empty($domain_string)) {
|
||||
// Ensure service has an empty domain entry for form binding
|
||||
$docker_compose_domains[$service_name]['domain'] = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$service_domains = str($domain_string)->explode(',')->map(fn ($d) => trim($d));
|
||||
|
||||
$preview_domains = [];
|
||||
foreach ($service_domains as $domain) {
|
||||
if (empty($domain)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$url = Url::fromString($domain);
|
||||
$template = $this->application->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2;
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
$preview_fqdn = "$schema://$preview_fqdn";
|
||||
$preview_domains[] = $preview_fqdn;
|
||||
}
|
||||
|
||||
if (! empty($preview_domains)) {
|
||||
$docker_compose_domains[$service_name]['domain'] = implode(',', $preview_domains);
|
||||
} else {
|
||||
// Ensure service has an empty domain entry for form binding
|
||||
$docker_compose_domains[$service_name]['domain'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$this->docker_compose_domains = json_encode($docker_compose_domains);
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,14 @@ public function realValue(): Attribute
|
|||
return null;
|
||||
}
|
||||
|
||||
return $this->get_real_environment_variables($this->value, $resource);
|
||||
$real_value = $this->get_real_environment_variables($this->value, $resource);
|
||||
if ($this->is_literal || $this->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($real_value);
|
||||
}
|
||||
|
||||
return $real_value;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ public function couldBeEnabled(): bool
|
|||
case 'azure':
|
||||
return filled($this->client_id) && filled($this->client_secret) && filled($this->tenant);
|
||||
case 'authentik':
|
||||
case 'clerk':
|
||||
return filled($this->client_id) && filled($this->client_secret) && filled($this->base_url);
|
||||
default:
|
||||
return filled($this->client_id) && filled($this->client_secret);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,18 @@ public function get_last_days_backup_status($days = 7)
|
|||
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
|
||||
}
|
||||
|
||||
public function executionsPaginated(int $skip = 0, int $take = 10)
|
||||
{
|
||||
$executions = $this->hasMany(ScheduledDatabaseBackupExecution::class)->orderBy('created_at', 'desc');
|
||||
$count = $executions->count();
|
||||
$executions = $executions->skip($skip)->take($take)->get();
|
||||
|
||||
return [
|
||||
'count' => $count,
|
||||
'executions' => $executions,
|
||||
];
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
if ($this->database) {
|
||||
|
|
|
|||
|
|
@ -887,7 +887,7 @@ public function privateKey()
|
|||
|
||||
public function muxFilename()
|
||||
{
|
||||
return $this->uuid;
|
||||
return 'mux_'.$this->uuid;
|
||||
}
|
||||
|
||||
public function team()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ProcessStatus;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
|
@ -9,6 +10,7 @@
|
|||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
|
|
@ -116,6 +118,18 @@ public function isExited()
|
|||
return (bool) str($this->status)->contains('exited');
|
||||
}
|
||||
|
||||
public function isStarting(): bool
|
||||
{
|
||||
try {
|
||||
$activity = Activity::where('properties->type_uuid', $this->uuid)->latest()->first();
|
||||
$status = data_get($activity, 'properties.status');
|
||||
|
||||
return $status === ProcessStatus::QUEUED->value || $status === ProcessStatus::IN_PROGRESS->value;
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function type()
|
||||
{
|
||||
return 'service';
|
||||
|
|
@ -159,6 +173,10 @@ public function deleteConnectedNetworks()
|
|||
|
||||
public function getStatusAttribute()
|
||||
{
|
||||
if ($this->isStarting()) {
|
||||
return 'starting:unhealthy';
|
||||
}
|
||||
|
||||
$applications = $this->applications;
|
||||
$databases = $this->databases;
|
||||
|
||||
|
|
@ -1242,26 +1260,17 @@ public function saveComposeConfigs()
|
|||
|
||||
return 3;
|
||||
});
|
||||
$envs = collect([]);
|
||||
foreach ($sorted as $env) {
|
||||
if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
|
||||
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
|
||||
} else {
|
||||
$real_value = $env->real_value;
|
||||
if ($env->version === '4.0.0-beta.239') {
|
||||
$real_value = $env->real_value;
|
||||
} else {
|
||||
if ($env->is_literal || $env->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
$real_value = escapeEnvVariables($env->real_value);
|
||||
}
|
||||
}
|
||||
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
|
||||
}
|
||||
$envs->push("{$env->key}={$env->real_value}");
|
||||
}
|
||||
if ($sorted->count() === 0) {
|
||||
if ($envs->count() === 0) {
|
||||
$commands[] = 'touch .env';
|
||||
} else {
|
||||
$envs_base64 = base64_encode($envs->implode("\n"));
|
||||
$commands[] = "echo '$envs_base64' | base64 -d | tee .env > /dev/null";
|
||||
}
|
||||
|
||||
instant_remote_process($commands, $this->server);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ public function isValid()
|
|||
return true;
|
||||
} else {
|
||||
$this->delete();
|
||||
$user = User::whereEmail($this->email)->first();
|
||||
if (filled($user)) {
|
||||
$user->deleteIfNotVerifiedAndForcePasswordReset();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,93 @@ protected static function boot()
|
|||
$new_team = Team::create($team);
|
||||
$user->teams()->attach($new_team, ['role' => 'owner']);
|
||||
});
|
||||
|
||||
static::deleting(function (User $user) {
|
||||
\DB::transaction(function () use ($user) {
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$user_alone_in_team = $team->members->count() === 1;
|
||||
|
||||
// Prevent deletion if user is alone in root team
|
||||
if ($team->id === 0 && $user_alone_in_team) {
|
||||
throw new \Exception('User is alone in the root team, cannot delete');
|
||||
}
|
||||
|
||||
if ($user_alone_in_team) {
|
||||
static::finalizeTeamDeletion($user, $team);
|
||||
// Delete any pending team invitations for this user
|
||||
TeamInvitation::whereEmail($user->email)->delete();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load the user's role for this team
|
||||
$userRole = $team->members->where('id', $user->id)->first()?->pivot?->role;
|
||||
|
||||
if ($userRole === 'owner') {
|
||||
$found_other_owner_or_admin = $team->members->filter(function ($member) use ($user) {
|
||||
return ($member->pivot->role === 'owner' || $member->pivot->role === 'admin') && $member->id !== $user->id;
|
||||
})->first();
|
||||
|
||||
if ($found_other_owner_or_admin) {
|
||||
$team->members()->detach($user->id);
|
||||
|
||||
continue;
|
||||
} else {
|
||||
$found_other_member_who_is_not_owner = $team->members->filter(function ($member) {
|
||||
return $member->pivot->role === 'member';
|
||||
})->first();
|
||||
|
||||
if ($found_other_member_who_is_not_owner) {
|
||||
$found_other_member_who_is_not_owner->pivot->role = 'owner';
|
||||
$found_other_member_who_is_not_owner->pivot->save();
|
||||
$team->members()->detach($user->id);
|
||||
} else {
|
||||
static::finalizeTeamDeletion($user, $team);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$team->members()->detach($user->id);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize team deletion by cleaning up all associated resources
|
||||
*/
|
||||
private static function finalizeTeamDeletion(User $user, Team $team)
|
||||
{
|
||||
$servers = $team->servers;
|
||||
foreach ($servers as $server) {
|
||||
$resources = $server->definedResources();
|
||||
foreach ($resources as $resource) {
|
||||
$resource->forceDelete();
|
||||
}
|
||||
$server->forceDelete();
|
||||
}
|
||||
|
||||
$projects = $team->projects;
|
||||
foreach ($projects as $project) {
|
||||
$project->forceDelete();
|
||||
}
|
||||
|
||||
$team->members()->detach($user->id);
|
||||
$team->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user if they are not verified and have a force password reset.
|
||||
* This is used to clean up users that have been invited, did not accept the invitation (and did not verify their email and have a force password reset).
|
||||
*/
|
||||
public function deleteIfNotVerifiedAndForcePasswordReset()
|
||||
{
|
||||
if ($this->hasVerifiedEmail() === false && $this->force_password_reset === true) {
|
||||
$this->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function recreate_personal_team()
|
||||
|
|
|
|||
66
app/Policies/S3StoragePolicy.php
Normal file
66
app/Policies/S3StoragePolicy.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
|
||||
class S3StoragePolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return $user->teams()->where('id', $storage->team_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Server $server): bool
|
||||
{
|
||||
return $user->teams()->get()->firstWhere('id', $server->team_id) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return $user->teams()->where('id', $storage->team_id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, S3Storage $storage): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,9 +9,12 @@
|
|||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Authentik\AuthentikExtendSocialite;
|
||||
use SocialiteProviders\Azure\AzureExtendSocialite;
|
||||
use SocialiteProviders\Clerk\ClerkExtendSocialite;
|
||||
use SocialiteProviders\Discord\DiscordExtendSocialite;
|
||||
use SocialiteProviders\Google\GoogleExtendSocialite;
|
||||
use SocialiteProviders\Infomaniak\InfomaniakExtendSocialite;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
use SocialiteProviders\Zitadel\ZitadelExtendSocialite;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -25,8 +28,11 @@ class EventServiceProvider extends ServiceProvider
|
|||
SocialiteWasCalled::class => [
|
||||
AzureExtendSocialite::class.'@handle',
|
||||
AuthentikExtendSocialite::class.'@handle',
|
||||
ClerkExtendSocialite::class.'@handle',
|
||||
DiscordExtendSocialite::class.'@handle',
|
||||
GoogleExtendSocialite::class.'@handle',
|
||||
InfomaniakExtendSocialite::class.'@handle',
|
||||
ZitadelExtendSocialite::class.'@handle',
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ protected function configureRateLimiting(): void
|
|||
return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip());
|
||||
}
|
||||
|
||||
return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip());
|
||||
return Limit::perMinute((int) config('api.rate_limit'))->by($request->user()?->id ?: $request->ip());
|
||||
});
|
||||
RateLimiter::for('5', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\View\Components\services;
|
||||
namespace App\View\Components\Services;
|
||||
|
||||
use App\Models\Service;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class advanced extends Component
|
||||
class Advanced extends Component
|
||||
{
|
||||
/**
|
||||
* Create a new component instance.
|
||||
|
|
@ -359,7 +359,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
|||
{
|
||||
$labels = collect([]);
|
||||
$labels->push('traefik.enable=true');
|
||||
$labels->push('traefik.http.middlewares.gzip.compress=true');
|
||||
if ($is_gzip_enabled) {
|
||||
$labels->push('traefik.http.middlewares.gzip.compress=true');
|
||||
}
|
||||
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
|
||||
|
||||
$is_http_basic_auth_enabled = $is_http_basic_auth_enabled && $http_basic_auth_username !== null && $http_basic_auth_password !== null;
|
||||
|
|
|
|||
|
|
@ -119,70 +119,62 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
|||
$resourceFqdns = str($resource->fqdn)->explode(',');
|
||||
if ($resourceFqdns->count() === 1) {
|
||||
$resourceFqdns = $resourceFqdns->first();
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$fqdn = Url::fromString($resourceFqdns);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$fqdn = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
if ($generatedEnv) {
|
||||
if ($path === '/') {
|
||||
$generatedEnv->value = $fqdn;
|
||||
} else {
|
||||
$generatedEnv->value = $fqdn.$path;
|
||||
}
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$fqdnValue = ($path === '/') ? $fqdn : $fqdn.$path;
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
if ($generatedEnv) {
|
||||
if ($path === '/') {
|
||||
$generatedEnv->value = $fqdn;
|
||||
} else {
|
||||
$generatedEnv->value = $fqdn.$path;
|
||||
}
|
||||
$generatedEnv->save();
|
||||
}
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$url = Url::fromString($fqdn);
|
||||
$port = $url->getPort();
|
||||
$path = $url->getPath();
|
||||
$url = $url->getHost();
|
||||
if ($generatedEnv) {
|
||||
$url = str($fqdn)->after('://');
|
||||
if ($path === '/') {
|
||||
$generatedEnv->value = $url;
|
||||
} else {
|
||||
$generatedEnv->value = $url.$path;
|
||||
}
|
||||
$generatedEnv->save();
|
||||
$urlValue = str($fqdn)->after('://');
|
||||
if ($path !== '/') {
|
||||
$urlValue = $urlValue.$path;
|
||||
}
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
if ($port) {
|
||||
$variableName = $variableName."_$port";
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
->first();
|
||||
if ($generatedEnv) {
|
||||
if ($path === '/') {
|
||||
$generatedEnv->value = $url;
|
||||
} else {
|
||||
$generatedEnv->value = $url.$path;
|
||||
}
|
||||
$generatedEnv->save();
|
||||
}
|
||||
EnvironmentVariable::updateOrCreate([
|
||||
'resourceable_type' => Service::class,
|
||||
'resourceable_id' => $resource->service_id,
|
||||
'key' => $variableName,
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_build_time' => false,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
} elseif ($resourceFqdns->count() > 1) {
|
||||
foreach ($resourceFqdns as $fqdn) {
|
||||
|
|
@ -243,7 +235,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
|||
$port_env_url->save();
|
||||
}
|
||||
} else {
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '');
|
||||
$variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
|
|
@ -254,7 +246,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
|||
$generatedEnv->value = $fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '');
|
||||
$variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', '_');
|
||||
$generatedEnv = EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', $variableName)
|
||||
|
|
@ -269,6 +261,17 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource)
|
|||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If FQDN is removed, delete the corresponding environment variables
|
||||
$serviceName = str($resource->name)->upper()->replace('-', '_');
|
||||
EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceName}%")
|
||||
->delete();
|
||||
EnvironmentVariable::where('resourceable_type', Service::class)
|
||||
->where('resourceable_id', $resource->service_id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceName}%")
|
||||
->delete();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e);
|
||||
|
|
|
|||
|
|
@ -599,7 +599,15 @@ function getTopLevelNetworks(Service|Application $resource)
|
|||
try {
|
||||
$yaml = Yaml::parse($resource->docker_compose_raw);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
// If the docker-compose.yml file is not valid, we will return the network name as the key
|
||||
$topLevelNetworks = collect([
|
||||
$resource->uuid => [
|
||||
'name' => $resource->uuid,
|
||||
'external' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
return $topLevelNetworks->keys();
|
||||
}
|
||||
$services = data_get($yaml, 'services');
|
||||
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
|
||||
|
|
@ -653,9 +661,16 @@ function getTopLevelNetworks(Service|Application $resource)
|
|||
try {
|
||||
$yaml = Yaml::parse($resource->docker_compose_raw);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
// If the docker-compose.yml file is not valid, we will return the network name as the key
|
||||
$topLevelNetworks = collect([
|
||||
$resource->uuid => [
|
||||
'name' => $resource->uuid,
|
||||
'external' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
return $topLevelNetworks->keys();
|
||||
}
|
||||
$server = $resource->destination->server;
|
||||
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
|
||||
$services = data_get($yaml, 'services');
|
||||
$definedNetwork = collect([$resource->uuid]);
|
||||
|
|
@ -2931,7 +2946,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
} catch (\Exception) {
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
$services = data_get($yaml, 'services', collect([]));
|
||||
$topLevel = collect([
|
||||
'volumes' => collect(data_get($yaml, 'volumes', [])),
|
||||
|
|
@ -2991,12 +3005,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
|
||||
if ($applicationFound) {
|
||||
$savedService = $applicationFound;
|
||||
// $savedService = ServiceDatabase::firstOrCreate([
|
||||
// 'name' => $applicationFound->name,
|
||||
// 'image' => $applicationFound->image,
|
||||
// 'service_id' => $applicationFound->service_id,
|
||||
// ]);
|
||||
// $applicationFound->delete();
|
||||
} else {
|
||||
$savedService = ServiceDatabase::firstOrCreate([
|
||||
'name' => $serviceName,
|
||||
|
|
@ -3007,15 +3015,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
$savedService = ServiceApplication::firstOrCreate([
|
||||
'name' => $serviceName,
|
||||
'service_id' => $resource->id,
|
||||
], [
|
||||
'is_gzip_enabled' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
// Check if image changed
|
||||
if ($savedService->image !== $image) {
|
||||
$savedService->image = $image;
|
||||
$savedService->save();
|
||||
}
|
||||
// Pocketbase does not need gzip for SSE.
|
||||
if (str($savedService->image)->contains('pocketbase') && $savedService->is_gzip_enabled) {
|
||||
$savedService->is_gzip_enabled = false;
|
||||
$savedService->save();
|
||||
}
|
||||
}
|
||||
|
||||
$environment = collect(data_get($service, 'environment', []));
|
||||
$buildArgs = collect(data_get($service, 'build.args', []));
|
||||
$environment = $environment->merge($buildArgs);
|
||||
|
|
@ -3034,7 +3049,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
// Get all SERVICE_ variables from keys and values
|
||||
$key = str($key);
|
||||
$value = str($value);
|
||||
|
||||
$regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/';
|
||||
preg_match_all($regex, $value, $valueMatches);
|
||||
if (count($valueMatches[1]) > 0) {
|
||||
|
|
@ -3048,7 +3062,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get magic environments where we need to preset the FQDN
|
||||
if ($key->startsWith('SERVICE_FQDN_')) {
|
||||
// SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
|
||||
|
|
@ -3060,12 +3073,19 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
$port = null;
|
||||
}
|
||||
if ($isApplication) {
|
||||
$fqdn = generateFqdn($server, "$uuid");
|
||||
$fqdn = $resource->fqdn;
|
||||
if (blank($resource->fqdn)) {
|
||||
$fqdn = generateFqdn($server, "$uuid");
|
||||
}
|
||||
} elseif ($isService) {
|
||||
if ($fqdnFor) {
|
||||
$fqdn = generateFqdn($server, "$fqdnFor-$uuid");
|
||||
if (blank($savedService->fqdn)) {
|
||||
if ($fqdnFor) {
|
||||
$fqdn = generateFqdn($server, "$fqdnFor-$uuid");
|
||||
} else {
|
||||
$fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
|
||||
}
|
||||
} else {
|
||||
$fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
|
||||
$fqdn = str($savedService->fqdn)->after('://')->before(':')->prepend(str($savedService->fqdn)->before('://')->append('://'))->value();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3090,7 +3110,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
|
||||
if (substr_count(str($key)->value(), '_') === 2) {
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $key->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
|
|
@ -3102,7 +3122,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
if (substr_count(str($key)->value(), '_') === 3) {
|
||||
$newKey = str($key)->beforeLast('_');
|
||||
$resource->environment_variables()->firstOrCreate([
|
||||
$resource->environment_variables()->updateOrCreate([
|
||||
'key' => $newKey->value(),
|
||||
'resourceable_type' => get_class($resource),
|
||||
'resourceable_id' => $resource->id,
|
||||
|
|
@ -3126,6 +3146,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
continue;
|
||||
}
|
||||
if ($command->value() === 'FQDN') {
|
||||
if ($isApplication && $resource->build_pack === 'dockercompose') {
|
||||
continue;
|
||||
}
|
||||
$fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
|
||||
if (str($fqdnFor)->contains('_')) {
|
||||
$fqdnFor = str($fqdnFor)->before('_');
|
||||
|
|
@ -3141,6 +3164,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
'is_preview' => false,
|
||||
]);
|
||||
} elseif ($command->value() === 'URL') {
|
||||
if ($isApplication && $resource->build_pack === 'dockercompose') {
|
||||
continue;
|
||||
}
|
||||
// For services, only generate URL if explicit FQDN is set
|
||||
if ($isService && blank($savedService->fqdn)) {
|
||||
continue;
|
||||
}
|
||||
$fqdnFor = $key->after('SERVICE_URL_')->lower()->value();
|
||||
if (str($fqdnFor)->contains('_')) {
|
||||
$fqdnFor = str($fqdnFor)->before('_');
|
||||
|
|
@ -3591,7 +3621,8 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
'is_required' => $isRequired,
|
||||
]);
|
||||
// Add the variable to the environment so it will be shown in the deployable compose file
|
||||
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->value;
|
||||
// $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where('resourceable_type', get_class($resource))->where('resourceable_id', $resource->id)->first()->real_value;
|
||||
$environment[$parsedKeyValue->value()] = $value;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
|
@ -3629,9 +3660,30 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
|
||||
if ($isApplication) {
|
||||
$domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]);
|
||||
if ($isPullRequest) {
|
||||
$preview = $resource->previews()->find($preview_id);
|
||||
$domains = collect(json_decode(data_get($preview, 'docker_compose_domains'))) ?? collect([]);
|
||||
} else {
|
||||
$domains = collect(json_decode($resource->docker_compose_domains)) ?? collect([]);
|
||||
}
|
||||
$fqdns = data_get($domains, "$serviceName.domain");
|
||||
if ($fqdns) {
|
||||
// Generate SERVICE_FQDN & SERVICE_URL for dockercompose
|
||||
if ($resource->build_pack === 'dockercompose') {
|
||||
foreach ($domains as $forServiceName => $domain) {
|
||||
$parsedDomain = data_get($domain, 'domain');
|
||||
if (filled($parsedDomain)) {
|
||||
$parsedDomain = str($parsedDomain)->explode(',')->first();
|
||||
$coolifyUrl = Url::fromString($parsedDomain);
|
||||
$coolifyScheme = $coolifyUrl->getScheme();
|
||||
$coolifyFqdn = $coolifyUrl->getHost();
|
||||
$coolifyUrl = $coolifyUrl->withScheme($coolifyScheme)->withHost($coolifyFqdn)->withPort(null);
|
||||
$coolifyEnvironments->put('SERVICE_URL_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyUrl->__toString());
|
||||
$coolifyEnvironments->put('SERVICE_FQDN_'.str($forServiceName)->upper()->replace('-', '_'), $coolifyFqdn);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the domain is set, we need to generate the FQDNs for the preview
|
||||
if (filled($fqdns)) {
|
||||
$fqdns = str($fqdns)->explode(',');
|
||||
if ($isPullRequest) {
|
||||
$preview = $resource->previews()->find($preview_id);
|
||||
|
|
@ -3663,7 +3715,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
$defaultLabels = defaultLabels(
|
||||
id: $resource->id,
|
||||
name: $containerName,
|
||||
|
|
@ -3673,6 +3724,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
type: 'application',
|
||||
environment: $resource->environment->name,
|
||||
);
|
||||
|
||||
} elseif ($isService) {
|
||||
if ($savedService->serviceType()) {
|
||||
$fqdns = generateServiceSpecificFqdns($savedService);
|
||||
|
|
@ -3694,10 +3746,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
|
|||
}
|
||||
// Add COOLIFY_FQDN & COOLIFY_URL to environment
|
||||
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
|
||||
$coolifyEnvironments->put('COOLIFY_URL', $fqdns->implode(','));
|
||||
$fqdnsWithoutPort = $fqdns->map(function ($fqdn) {
|
||||
return str($fqdn)->after('://')->before(':')->prepend(str($fqdn)->before('://')->append('://'));
|
||||
});
|
||||
$coolifyEnvironments->put('COOLIFY_URL', $fqdnsWithoutPort->implode(','));
|
||||
|
||||
$urls = $fqdns->map(function ($fqdn) {
|
||||
return str($fqdn)->replace('http://', '')->replace('https://', '');
|
||||
return str($fqdn)->replace('http://', '')->replace('https://', '')->before(':');
|
||||
});
|
||||
$coolifyEnvironments->put('COOLIFY_FQDN', $urls->implode(','));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,15 +22,26 @@ function get_socialite_provider(string $provider)
|
|||
return Socialite::driver('azure')->setConfig($azure_config);
|
||||
}
|
||||
|
||||
if ($provider == 'authentik') {
|
||||
$authentik_config = new \SocialiteProviders\Manager\Config(
|
||||
if ($provider == 'authentik' || $provider == 'clerk') {
|
||||
$authentik_clerk_config = new \SocialiteProviders\Manager\Config(
|
||||
$oauth_setting->client_id,
|
||||
$oauth_setting->client_secret,
|
||||
$oauth_setting->redirect_uri,
|
||||
['base_url' => $oauth_setting->base_url],
|
||||
);
|
||||
|
||||
return Socialite::driver('authentik')->setConfig($authentik_config);
|
||||
return Socialite::driver($provider)->setConfig($authentik_clerk_config);
|
||||
}
|
||||
|
||||
if ($provider == 'zitadel') {
|
||||
$zitadel_config = new \SocialiteProviders\Manager\Config(
|
||||
$oauth_setting->client_id,
|
||||
$oauth_setting->client_secret,
|
||||
$oauth_setting->redirect_uri,
|
||||
['base_url' => $oauth_setting->base_url],
|
||||
);
|
||||
|
||||
return Socialite::driver('zitadel')->setConfig($zitadel_config);
|
||||
}
|
||||
|
||||
if ($provider == 'google') {
|
||||
|
|
@ -53,6 +64,7 @@ function get_socialite_provider(string $provider)
|
|||
|
||||
$provider_class_map = [
|
||||
'bitbucket' => \Laravel\Socialite\Two\BitbucketProvider::class,
|
||||
'discord' => \SocialiteProviders\Discord\Provider::class,
|
||||
'github' => \Laravel\Socialite\Two\GithubProvider::class,
|
||||
'gitlab' => \Laravel\Socialite\Two\GitlabProvider::class,
|
||||
'infomaniak' => \SocialiteProviders\Infomaniak\Provider::class,
|
||||
|
|
|
|||
|
|
@ -13,62 +13,65 @@
|
|||
"require": {
|
||||
"php": "^8.4",
|
||||
"danharrin/livewire-rate-limiting": "^2.1.0",
|
||||
"doctrine/dbal": "^4.2.2",
|
||||
"guzzlehttp/guzzle": "^7.9.2",
|
||||
"laravel/fortify": "^1.25.4",
|
||||
"laravel/framework": "^12.4.1",
|
||||
"laravel/horizon": "^5.30.3",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/prompts": "^0.3.5|^0.3.5|^0.3.5",
|
||||
"laravel/sanctum": "^4.0.8",
|
||||
"laravel/socialite": "^5.18.0",
|
||||
"doctrine/dbal": "^4.3.0",
|
||||
"guzzlehttp/guzzle": "^7.9.3",
|
||||
"laravel/fortify": "^1.27.0",
|
||||
"laravel/framework": "^12.20.0",
|
||||
"laravel/horizon": "^5.33.1",
|
||||
"laravel/pail": "^1.2.3",
|
||||
"laravel/prompts": "^0.3.6|^0.3.6|^0.3.6",
|
||||
"laravel/sanctum": "^4.1.2",
|
||||
"laravel/socialite": "^5.21.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"laravel/ui": "^4.6.1",
|
||||
"lcobucci/jwt": "^5.5.0",
|
||||
"league/flysystem-aws-s3-v3": "^3.29",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"livewire/livewire": "^3.5.20",
|
||||
"league/flysystem-sftp-v3": "^3.30",
|
||||
"livewire/livewire": "^3.6.4",
|
||||
"log1x/laravel-webfonts": "^2.0.1",
|
||||
"lorisleiva/laravel-actions": "^2.8.6",
|
||||
"lorisleiva/laravel-actions": "^2.9.0",
|
||||
"nubs/random-name-generator": "^2.2",
|
||||
"phpseclib/phpseclib": "^3.0.43",
|
||||
"pion/laravel-chunk-upload": "^1.5.4",
|
||||
"phpseclib/phpseclib": "^3.0.46",
|
||||
"pion/laravel-chunk-upload": "^1.5.6",
|
||||
"poliander/cron": "^3.2.1",
|
||||
"purplepixie/phpdns": "^2.2",
|
||||
"pusher/pusher-php-server": "^7.2.7",
|
||||
"resend/resend-laravel": "^0.17.0",
|
||||
"sentry/sentry-laravel": "^4.13",
|
||||
"resend/resend-laravel": "^0.19.0",
|
||||
"sentry/sentry-laravel": "^4.15.1",
|
||||
"socialiteproviders/authentik": "^5.2",
|
||||
"socialiteproviders/clerk": "^5.0",
|
||||
"socialiteproviders/discord": "^4.2",
|
||||
"socialiteproviders/google": "^4.1",
|
||||
"socialiteproviders/infomaniak": "^4.0",
|
||||
"socialiteproviders/microsoft-azure": "^5.2",
|
||||
"spatie/laravel-activitylog": "^4.10.1",
|
||||
"spatie/laravel-data": "^4.13.1",
|
||||
"spatie/laravel-ray": "^1.39.1",
|
||||
"socialiteproviders/zitadel": "^4.2",
|
||||
"spatie/laravel-activitylog": "^4.10.2",
|
||||
"spatie/laravel-data": "^4.17.0",
|
||||
"spatie/laravel-ray": "^1.40.2",
|
||||
"spatie/laravel-schemaless-attributes": "^2.5.1",
|
||||
"spatie/url": "^2.4",
|
||||
"stevebauman/purify": "^6.3",
|
||||
"stripe/stripe-php": "^16.5.1",
|
||||
"symfony/yaml": "^7.2.3",
|
||||
"stevebauman/purify": "^6.3.1",
|
||||
"stripe/stripe-php": "^16.6.0",
|
||||
"symfony/yaml": "^7.3.1",
|
||||
"visus/cuid2": "^4.1.0",
|
||||
"yosymfony/toml": "^1.0.4",
|
||||
"zircote/swagger-php": "^5.0.5"
|
||||
"zircote/swagger-php": "^5.1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.15.1",
|
||||
"driftingly/rector-laravel": "^2.0.2",
|
||||
"barryvdh/laravel-debugbar": "^3.15.4",
|
||||
"driftingly/rector-laravel": "^2.0.5",
|
||||
"fakerphp/faker": "^1.24.1",
|
||||
"laravel/dusk": "^8.3.1",
|
||||
"laravel/pint": "^1.21",
|
||||
"laravel/telescope": "^5.5",
|
||||
"laravel/dusk": "^8.3.3",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/telescope": "^5.10",
|
||||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.6.1",
|
||||
"pestphp/pest": "^3.8.0",
|
||||
"phpstan/phpstan": "^2.1.6",
|
||||
"rector/rector": "^2.0.9",
|
||||
"nunomaduro/collision": "^8.8.2",
|
||||
"pestphp/pest": "^3.8.2",
|
||||
"phpstan/phpstan": "^2.1.18",
|
||||
"rector/rector": "^2.1.2",
|
||||
"serversideup/spin": "^3.0.2",
|
||||
"spatie/laravel-ignition": "^2.9.1",
|
||||
"symfony/http-client": "^7.2.3"
|
||||
"symfony/http-client": "^7.3.1"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
|
|
|
|||
1050
composer.lock
generated
1050
composer.lock
generated
File diff suppressed because it is too large
Load diff
5
config/api.php
Normal file
5
config/api.php
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'rate_limit' => env('API_RATE_LIMIT', 200),
|
||||
];
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.420',
|
||||
'helper_version' => '1.0.8',
|
||||
'realtime_version' => '1.0.9',
|
||||
'version' => '4.0.0-beta.420.7',
|
||||
'helper_version' => '1.0.9',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
'autoupdate' => env('AUTOUPDATE'),
|
||||
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
|
||||
|
|
|
|||
|
|
@ -182,14 +182,15 @@
|
|||
'defaults' => [
|
||||
's6' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default'],
|
||||
'balance' => env('HORIZON_BALANCE', 'auto'),
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'balance' => env('HORIZON_BALANCE', 'false'),
|
||||
'queue' => env('HORIZON_QUEUES', 'high,default'),
|
||||
'maxTime' => 3600,
|
||||
'maxJobs' => 400,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 3560,
|
||||
'nice' => 0,
|
||||
'sleep' => 3,
|
||||
'timeout' => 3600,
|
||||
],
|
||||
],
|
||||
|
||||
|
|
@ -198,7 +199,7 @@
|
|||
's6' => [
|
||||
'autoScalingStrategy' => 'size',
|
||||
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
|
||||
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
|
||||
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4),
|
||||
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
|
||||
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
|
||||
],
|
||||
|
|
@ -208,7 +209,7 @@
|
|||
's6' => [
|
||||
'autoScalingStrategy' => 'size',
|
||||
'minProcesses' => env('HORIZON_MIN_PROCESSES', 1),
|
||||
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6),
|
||||
'maxProcesses' => env('HORIZON_MAX_PROCESSES', 4),
|
||||
'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1),
|
||||
'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -118,6 +118,20 @@
|
|||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
'scheduled' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 1,
|
||||
],
|
||||
|
||||
'scheduled-errors' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/scheduled-errors.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 7,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -46,6 +46,13 @@
|
|||
'redirect' => env('AUTHENTIK_REDIRECT_URI'),
|
||||
],
|
||||
|
||||
'clerk' => [
|
||||
'client_id' => env('CLERK_CLIENT_ID'),
|
||||
'client_secret' => env('CLERK_CLIENT_SECRET'),
|
||||
'redirect' => env('CLERK_REDIRECT_URI'),
|
||||
'base_url' => env('CLERK_BASE_URL'),
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
|
|
@ -53,4 +60,11 @@
|
|||
'tenant' => env('GOOGLE_TENANT'),
|
||||
],
|
||||
|
||||
'zitadel' => [
|
||||
'client_id' => env('ZITADEL_CLIENT_ID'),
|
||||
'client_secret' => env('ZITADEL_CLIENT_SECRET'),
|
||||
'redirect' => env('ZITADEL_REDIRECT_URI'),
|
||||
'base_url' => env('ZITADEL_BASE_URL'),
|
||||
]
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->boolean('is_sponsorship_popup_enabled')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('instance_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('is_sponsorship_popup_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
try {
|
||||
// Add specific index for type_uuid queries with ordering
|
||||
DB::statement('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_type_uuid_created_at ON activity_log ((properties->>\'type_uuid\'), created_at DESC)');
|
||||
|
||||
// Add specific index for status queries on properties
|
||||
DB::statement('CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_activity_properties_status ON activity_log ((properties->>\'status\'))');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error adding optimized indexes to activity_log: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
try {
|
||||
DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_type_uuid_created_at');
|
||||
DB::statement('DROP INDEX CONCURRENTLY IF EXISTS idx_activity_properties_status');
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error dropping optimized indexes from activity_log: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('scheduled_database_backups', function (Blueprint $table) {
|
||||
$table->integer('timeout')->default(3600);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -17,11 +17,14 @@ public function run(): void
|
|||
$providers = collect([
|
||||
'azure',
|
||||
'bitbucket',
|
||||
'clerk',
|
||||
'discord',
|
||||
'github',
|
||||
'gitlab',
|
||||
'google',
|
||||
'authentik',
|
||||
'infomaniak',
|
||||
'zitadel',
|
||||
]);
|
||||
|
||||
$isOauthSeeded = OauthSetting::count() > 0;
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ ARG BASE_IMAGE=alpine:3.21
|
|||
# https://download.docker.com/linux/static/stable/
|
||||
ARG DOCKER_VERSION=28.0.0
|
||||
# https://github.com/docker/compose/releases
|
||||
ARG DOCKER_COMPOSE_VERSION=2.34.0
|
||||
ARG DOCKER_COMPOSE_VERSION=2.38.2
|
||||
# https://github.com/docker/buildx/releases
|
||||
ARG DOCKER_BUILDX_VERSION=0.22.0
|
||||
ARG DOCKER_BUILDX_VERSION=0.25.0
|
||||
# https://github.com/buildpacks/pack/releases
|
||||
ARG PACK_VERSION=0.37.0
|
||||
ARG PACK_VERSION=0.38.2
|
||||
# https://github.com/railwayapp/nixpacks/releases
|
||||
ARG NIXPACKS_VERSION=1.34.1
|
||||
ARG NIXPACKS_VERSION=1.39.0
|
||||
# https://github.com/minio/mc/releases
|
||||
ARG MINIO_VERSION=RELEASE.2025-03-12T17-29-24Z
|
||||
ARG MINIO_VERSION=RELEASE.2025-05-21T01-59-54Z
|
||||
|
||||
FROM minio/mc:${MINIO_VERSION} AS minio-client
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# https://github.com/soketi/soketi/releases
|
||||
ARG SOKETI_VERSION=1.6-16-alpine
|
||||
# https://github.com/cloudflare/cloudflared/releases
|
||||
ARG CLOUDFLARED_VERSION=2025.5.0
|
||||
ARG CLOUDFLARED_VERSION=2025.7.0
|
||||
|
||||
FROM quay.io/soketi/soketi:${SOKETI_VERSION}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue