Merge branch 'next' into add/rivet-dev
This commit is contained in:
commit
439ecc277d
17 changed files with 838 additions and 64 deletions
|
|
@ -4,11 +4,42 @@
|
||||||
|
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Spatie\Url\Url;
|
use Spatie\Url\Url;
|
||||||
|
|
||||||
class TrustHosts extends Middleware
|
class TrustHosts extends Middleware
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming request.
|
||||||
|
*
|
||||||
|
* Skip host validation for certain routes:
|
||||||
|
* - Terminal auth routes (called by realtime container)
|
||||||
|
* - API routes (use token-based authentication, not host validation)
|
||||||
|
* - Webhook endpoints (use cryptographic signature validation)
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, $next)
|
||||||
|
{
|
||||||
|
// Skip host validation for these routes
|
||||||
|
if ($request->is(
|
||||||
|
'terminal/auth',
|
||||||
|
'terminal/auth/ips',
|
||||||
|
'api/*',
|
||||||
|
'webhooks/*'
|
||||||
|
)) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip host validation if no FQDN is configured (initial setup)
|
||||||
|
$fqdnHost = Cache::get('instance_settings_fqdn_host');
|
||||||
|
if ($fqdnHost === '' || $fqdnHost === null) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other routes, use parent's host validation
|
||||||
|
return parent::handle($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the host patterns that should be trusted.
|
* Get the host patterns that should be trusted.
|
||||||
*
|
*
|
||||||
|
|
@ -44,6 +75,19 @@ public function hosts(): array
|
||||||
$trustedHosts[] = $fqdnHost;
|
$trustedHosts[] = $fqdnHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trust the APP_URL host itself (not just subdomains)
|
||||||
|
$appUrl = config('app.url');
|
||||||
|
if ($appUrl) {
|
||||||
|
try {
|
||||||
|
$appUrlHost = parse_url($appUrl, PHP_URL_HOST);
|
||||||
|
if ($appUrlHost && ! in_array($appUrlHost, $trustedHosts, true)) {
|
||||||
|
$trustedHosts[] = $appUrlHost;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Trust all subdomains of APP_URL as fallback
|
// Trust all subdomains of APP_URL as fallback
|
||||||
$trustedHosts[] = $this->allSubdomainsOfApplicationUrl();
|
$trustedHosts[] = $this->allSubdomainsOfApplicationUrl();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,10 @@ private function deploy_dockerimage_buildpack()
|
||||||
$this->generate_image_names();
|
$this->generate_image_names();
|
||||||
$this->prepare_builder_image();
|
$this->prepare_builder_image();
|
||||||
$this->generate_compose_file();
|
$this->generate_compose_file();
|
||||||
|
|
||||||
|
// Save runtime environment variables (including empty .env file if no variables defined)
|
||||||
|
$this->save_runtime_environment_variables();
|
||||||
|
|
||||||
$this->rolling_update();
|
$this->rolling_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1222,9 +1226,9 @@ private function save_runtime_environment_variables()
|
||||||
|
|
||||||
// Handle empty environment variables
|
// Handle empty environment variables
|
||||||
if ($environment_variables->isEmpty()) {
|
if ($environment_variables->isEmpty()) {
|
||||||
// For Docker Compose, we need to create an empty .env file
|
// For Docker Compose and Docker Image, we need to create an empty .env file
|
||||||
// because we always reference it in the compose file
|
// because we always reference it in the compose file
|
||||||
if ($this->build_pack === 'dockercompose') {
|
if ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerimage') {
|
||||||
$this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).');
|
$this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).');
|
||||||
|
|
||||||
// Create empty .env file
|
// Create empty .env file
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ public function submit()
|
||||||
$this->validate();
|
$this->validate();
|
||||||
$this->application->save();
|
$this->application->save();
|
||||||
$this->application->refresh();
|
$this->application->refresh();
|
||||||
$this->syncData(false);
|
$this->syncFromModel();
|
||||||
updateCompose($this->application);
|
updateCompose($this->application);
|
||||||
if (str($this->application->fqdn)->contains(',')) {
|
if (str($this->application->fqdn)->contains(',')) {
|
||||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||||
|
|
@ -96,7 +96,7 @@ public function submit()
|
||||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||||
if ($originalFqdn !== $this->application->fqdn) {
|
if ($originalFqdn !== $this->application->fqdn) {
|
||||||
$this->application->fqdn = $originalFqdn;
|
$this->application->fqdn = $originalFqdn;
|
||||||
$this->syncData(false);
|
$this->syncFromModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
|
|
|
||||||
|
|
@ -1804,7 +1804,9 @@ public function getFilesFromServer(bool $isInit = false)
|
||||||
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
||||||
{
|
{
|
||||||
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
||||||
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
|
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
|
||||||
|
|
||||||
|
if ($hasHealthcheck && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||||
$healthcheckCommand = null;
|
$healthcheckCommand = null;
|
||||||
$lines = $dockerfile->toArray();
|
$lines = $dockerfile->toArray();
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
|
|
@ -1845,6 +1847,14 @@ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} elseif (! $hasHealthcheck && $this->custom_healthcheck_found) {
|
||||||
|
// HEALTHCHECK was removed from Dockerfile, reset to defaults
|
||||||
|
$this->custom_healthcheck_found = false;
|
||||||
|
$this->health_check_interval = 5;
|
||||||
|
$this->health_check_timeout = 5;
|
||||||
|
$this->health_check_retries = 10;
|
||||||
|
$this->health_check_start_period = 5;
|
||||||
|
$this->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.436',
|
'version' => '4.0.0-beta.438',
|
||||||
'helper_version' => '1.0.11',
|
'helper_version' => '1.0.11',
|
||||||
'realtime_version' => '1.0.10',
|
'realtime_version' => '1.0.10',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
|
|
|
||||||
306
openapi.json
306
openapi.json
|
|
@ -3356,6 +3356,137 @@
|
||||||
"bearerAuth": []
|
"bearerAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Databases"
|
||||||
|
],
|
||||||
|
"summary": "Create Backup",
|
||||||
|
"description": "Create a new scheduled backup configuration for a database",
|
||||||
|
"operationId": "create-database-backup",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "UUID of the database.",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Backup configuration data",
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"required": [
|
||||||
|
"frequency"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"frequency": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the backup is enabled",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"save_s3": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to save backups to S3",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"s3_storage_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "S3 storage UUID (required if save_s3 is true)"
|
||||||
|
},
|
||||||
|
"databases_to_backup": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma separated list of databases to backup"
|
||||||
|
},
|
||||||
|
"dump_all": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to dump all databases",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"backup_now": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether to trigger backup immediately after creation"
|
||||||
|
},
|
||||||
|
"database_backup_retention_amount_locally": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of backups to retain locally"
|
||||||
|
},
|
||||||
|
"database_backup_retention_days_locally": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of days to retain backups locally"
|
||||||
|
},
|
||||||
|
"database_backup_retention_max_storage_locally": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Max storage (MB) for local backups"
|
||||||
|
},
|
||||||
|
"database_backup_retention_amount_s3": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of backups to retain in S3"
|
||||||
|
},
|
||||||
|
"database_backup_retention_days_s3": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of days to retain backups in S3"
|
||||||
|
},
|
||||||
|
"database_backup_retention_max_storage_s3": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Max storage (MB) for S3 backups"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Backup configuration created successfully",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"example": "550e8400-e29b-41d4-a716-446655440000"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Backup configuration created successfully."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#\/components\/responses\/422"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"\/databases\/{uuid}": {
|
"\/databases\/{uuid}": {
|
||||||
|
|
@ -5381,6 +5512,96 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"\/deployments\/{uuid}\/cancel": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Deployments"
|
||||||
|
],
|
||||||
|
"summary": "Cancel",
|
||||||
|
"description": "Cancel a deployment by UUID.",
|
||||||
|
"operationId": "cancel-deployment-by-uuid",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "uuid",
|
||||||
|
"in": "path",
|
||||||
|
"description": "Deployment UUID",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Deployment cancelled successfully.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Deployment cancelled successfully."
|
||||||
|
},
|
||||||
|
"deployment_uuid": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cm37r6cqj000008jm0veg5tkm"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "cancelled-by-user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Deployment cannot be cancelled (already finished\/failed\/cancelled).",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Deployment cannot be cancelled. Current status: finished"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "User doesn't have permission to cancel this deployment.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "You do not have permission to cancel this deployment."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#\/components\/responses\/404"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"\/deploy": {
|
"\/deploy": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -5538,6 +5759,91 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"\/github-apps": {
|
"\/github-apps": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"GitHub Apps"
|
||||||
|
],
|
||||||
|
"summary": "List",
|
||||||
|
"description": "List all GitHub apps.",
|
||||||
|
"operationId": "list-github-apps",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of GitHub apps.",
|
||||||
|
"content": {
|
||||||
|
"application\/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"organization": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"api_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"html_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"custom_user": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"custom_port": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"installation_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"client_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"private_key_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"is_system_wide": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"is_public": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"team_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"$ref": "#\/components\/responses\/401"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#\/components\/responses\/400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"GitHub Apps"
|
"GitHub Apps"
|
||||||
|
|
|
||||||
160
openapi.yaml
160
openapi.yaml
|
|
@ -2130,6 +2130,94 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Databases
|
||||||
|
summary: 'Create Backup'
|
||||||
|
description: 'Create a new scheduled backup configuration for a database'
|
||||||
|
operationId: create-database-backup
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'UUID of the database.'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
requestBody:
|
||||||
|
description: 'Backup configuration data'
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
required:
|
||||||
|
- frequency
|
||||||
|
properties:
|
||||||
|
frequency:
|
||||||
|
type: string
|
||||||
|
description: 'Backup frequency (cron expression or: every_minute, hourly, daily, weekly, monthly, yearly)'
|
||||||
|
enabled:
|
||||||
|
type: boolean
|
||||||
|
description: 'Whether the backup is enabled'
|
||||||
|
default: true
|
||||||
|
save_s3:
|
||||||
|
type: boolean
|
||||||
|
description: 'Whether to save backups to S3'
|
||||||
|
default: false
|
||||||
|
s3_storage_uuid:
|
||||||
|
type: string
|
||||||
|
description: 'S3 storage UUID (required if save_s3 is true)'
|
||||||
|
databases_to_backup:
|
||||||
|
type: string
|
||||||
|
description: 'Comma separated list of databases to backup'
|
||||||
|
dump_all:
|
||||||
|
type: boolean
|
||||||
|
description: 'Whether to dump all databases'
|
||||||
|
default: false
|
||||||
|
backup_now:
|
||||||
|
type: boolean
|
||||||
|
description: 'Whether to trigger backup immediately after creation'
|
||||||
|
database_backup_retention_amount_locally:
|
||||||
|
type: integer
|
||||||
|
description: 'Number of backups to retain locally'
|
||||||
|
database_backup_retention_days_locally:
|
||||||
|
type: integer
|
||||||
|
description: 'Number of days to retain backups locally'
|
||||||
|
database_backup_retention_max_storage_locally:
|
||||||
|
type: integer
|
||||||
|
description: 'Max storage (MB) for local backups'
|
||||||
|
database_backup_retention_amount_s3:
|
||||||
|
type: integer
|
||||||
|
description: 'Number of backups to retain in S3'
|
||||||
|
database_backup_retention_days_s3:
|
||||||
|
type: integer
|
||||||
|
description: 'Number of days to retain backups in S3'
|
||||||
|
database_backup_retention_max_storage_s3:
|
||||||
|
type: integer
|
||||||
|
description: 'Max storage (MB) for S3 backups'
|
||||||
|
type: object
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: 'Backup configuration created successfully'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
uuid: { type: string, format: uuid, example: 550e8400-e29b-41d4-a716-446655440000 }
|
||||||
|
message: { type: string, example: 'Backup configuration created successfully.' }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
'422':
|
||||||
|
$ref: '#/components/responses/422'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
'/databases/{uuid}':
|
'/databases/{uuid}':
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -3532,6 +3620,55 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
'/deployments/{uuid}/cancel':
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Deployments
|
||||||
|
summary: Cancel
|
||||||
|
description: 'Cancel a deployment by UUID.'
|
||||||
|
operationId: cancel-deployment-by-uuid
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: uuid
|
||||||
|
in: path
|
||||||
|
description: 'Deployment UUID'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'Deployment cancelled successfully.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
message: { type: string, example: 'Deployment cancelled successfully.' }
|
||||||
|
deployment_uuid: { type: string, example: cm37r6cqj000008jm0veg5tkm }
|
||||||
|
status: { type: string, example: cancelled-by-user }
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: 'Deployment cannot be cancelled (already finished/failed/cancelled).'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
message: { type: string, example: 'Deployment cannot be cancelled. Current status: finished' }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'403':
|
||||||
|
description: "User doesn't have permission to cancel this deployment."
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
message: { type: string, example: 'You do not have permission to cancel this deployment.' }
|
||||||
|
type: object
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/404'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/deploy:
|
/deploy:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -3631,6 +3768,29 @@ paths:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
/github-apps:
|
/github-apps:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'GitHub Apps'
|
||||||
|
summary: List
|
||||||
|
description: 'List all GitHub apps.'
|
||||||
|
operationId: list-github-apps
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 'List of GitHub apps.'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
properties: { id: { type: integer }, uuid: { type: string }, name: { type: string }, organization: { type: string, nullable: true }, api_url: { type: string }, html_url: { type: string }, custom_user: { type: string }, custom_port: { type: integer }, app_id: { type: integer }, installation_id: { type: integer }, client_id: { type: string }, private_key_id: { type: integer }, is_system_wide: { type: boolean }, is_public: { type: boolean }, team_id: { type: integer }, type: { type: string } }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/401'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/400'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- 'GitHub Apps'
|
- 'GitHub Apps'
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,15 @@ @utility select {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply input-select;
|
@apply input-select;
|
||||||
@apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
@apply focus-visible:outline-none focus-visible:border-l-4 focus-visible:border-l-coollabs dark:focus-visible:border-l-warning;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23000000'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1rem 1rem;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
|
||||||
|
&:where(.dark, .dark *) {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23ffffff'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility button {
|
@utility button {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden">
|
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
|
||||||
<div class="flex items-center gap-3 flex-shrink-0">
|
<div class="flex items-center gap-3 flex-shrink-0">
|
||||||
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
|
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||||
<livewire:switch-team />
|
<livewire:switch-team />
|
||||||
|
|
|
||||||
|
|
@ -99,13 +99,11 @@
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button @click="dropdownOpen = !dropdownOpen"
|
<button @click="dropdownOpen = !dropdownOpen"
|
||||||
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
class="relative p-2 dark:text-neutral-400 hover:dark:text-white transition-colors cursor-pointer"
|
||||||
title="Settings">
|
title="Preferences">
|
||||||
<!-- Gear Icon -->
|
<!-- Sliders Icon -->
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Settings">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="Preferences">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Unread Count Badge -->
|
<!-- Unread Count Badge -->
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,41 @@
|
||||||
<div>
|
<div>
|
||||||
<x-slot:title>
|
<x-slot:title>
|
||||||
Terminal | Coolify
|
Terminal | Coolify
|
||||||
</x-slot>
|
</x-slot>
|
||||||
<h1>Terminal</h1>
|
<h1>Terminal</h1>
|
||||||
<div class="flex gap-2 items-end subtitle">
|
<div class="flex gap-2 items-end subtitle">
|
||||||
<div>Execute commands on your servers and containers without leaving the browser.</div>
|
<div>Execute commands on your servers and containers without leaving the browser.</div>
|
||||||
<x-helper
|
<x-helper
|
||||||
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
|
helper="If you're having trouble connecting to your server, make sure that the port is open.<br><br><a class='underline' href='https://coolify.io/docs/knowledge-base/server/firewall/#terminal' target='_blank'>Documentation</a>"></x-helper>
|
||||||
</div>
|
</div>
|
||||||
<div x-init="$wire.loadContainers()">
|
<div x-init="$wire.loadContainers()">
|
||||||
@if ($isLoadingContainers)
|
@if ($isLoadingContainers)
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<x-loading text="Loading servers and containers..." />
|
<x-loading text="Loading servers and containers..." />
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
@if ($servers->count() > 0)
|
|
||||||
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
|
|
||||||
wire:submit="$dispatchSelf('connectToContainer')">
|
|
||||||
<x-forms.datalist id="selected_uuid" required wire:model.live="selected_uuid" placeholder="Search servers or containers...">
|
|
||||||
@foreach ($servers as $server)
|
|
||||||
@if ($loop->first)
|
|
||||||
<option disabled value="default">Select a server or container</option>
|
|
||||||
@endif
|
|
||||||
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
|
||||||
@foreach ($containers as $container)
|
|
||||||
@if ($container['server_uuid'] == $server->uuid)
|
|
||||||
<option value="{{ $container['uuid'] }}">
|
|
||||||
{{ $server->name }} -> {{ $container['name'] }}
|
|
||||||
</option>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
|
||||||
@endforeach
|
|
||||||
</x-forms.datalist>
|
|
||||||
<x-forms.button type="submit">Connect</x-forms.button>
|
|
||||||
</form>
|
|
||||||
@else
|
@else
|
||||||
<div>No servers with terminal access found.</div>
|
@if ($servers->count() > 0)
|
||||||
|
<form class="flex flex-col gap-2 justify-center xl:items-end xl:flex-row"
|
||||||
|
wire:submit="$dispatchSelf('connectToContainer')">
|
||||||
|
<x-forms.select id="selected_uuid" required wire:model.live="selected_uuid">
|
||||||
|
<option value="default">Select a server or container</option>
|
||||||
|
@foreach ($servers as $server)
|
||||||
|
<option value="{{ $server->uuid }}">{{ $server->name }}</option>
|
||||||
|
@foreach ($containers as $container)
|
||||||
|
@if ($container['server_uuid'] == $server->uuid)
|
||||||
|
<option value="{{ $container['uuid'] }}">
|
||||||
|
{{ $server->name }} -> {{ $container['name'] }}
|
||||||
|
</option>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endforeach
|
||||||
|
</x-forms.select>
|
||||||
|
<x-forms.button type="submit">Connect</x-forms.button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<div>No servers with terminal access found.</div>
|
||||||
|
@endif
|
||||||
@endif
|
@endif
|
||||||
@endif
|
<livewire:project.shared.terminal />
|
||||||
<livewire:project.shared.terminal />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# ignore: true
|
||||||
# documentation: https://min.io/docs/minio/container/index.html
|
# documentation: https://min.io/docs/minio/container/index.html
|
||||||
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
||||||
# category: storage
|
# category: storage
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -227,3 +227,84 @@
|
||||||
// Should only contain APP_URL pattern, not any FQDN
|
// Should only contain APP_URL pattern, not any FQDN
|
||||||
expect($hosts2)->not->toBeEmpty();
|
expect($hosts2)->not->toBeEmpty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('skips host validation for terminal auth routes', function () {
|
||||||
|
// These routes should be accessible with any Host header (for internal container communication)
|
||||||
|
$response = $this->postJson('/terminal/auth', [], [
|
||||||
|
'Host' => 'coolify:8080', // Internal Docker host
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should not get 400 Bad Host (might get 401 Unauthorized instead)
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips host validation for terminal auth ips route', function () {
|
||||||
|
// These routes should be accessible with any Host header (for internal container communication)
|
||||||
|
$response = $this->postJson('/terminal/auth/ips', [], [
|
||||||
|
'Host' => 'soketi:6002', // Another internal Docker host
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should not get 400 Bad Host (might get 401 Unauthorized instead)
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still enforces host validation for non-terminal routes', function () {
|
||||||
|
InstanceSettings::updateOrCreate(
|
||||||
|
['id' => 0],
|
||||||
|
['fqdn' => 'https://coolify.example.com']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Regular routes should still validate Host header
|
||||||
|
$response = $this->get('/', [
|
||||||
|
'Host' => 'evil.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Should get 400 Bad Host for untrusted host
|
||||||
|
expect($response->status())->toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips host validation for API routes', function () {
|
||||||
|
// All API routes use token-based auth (Sanctum), not host validation
|
||||||
|
// They should be accessible from any host (mobile apps, CLI tools, scripts)
|
||||||
|
|
||||||
|
// Test health check endpoint
|
||||||
|
$response = $this->get('/api/health', [
|
||||||
|
'Host' => 'internal-lb.local',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
|
||||||
|
// Test v1 health check
|
||||||
|
$response = $this->get('/api/v1/health', [
|
||||||
|
'Host' => '10.0.0.5',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
|
||||||
|
// Test feedback endpoint
|
||||||
|
$response = $this->post('/api/feedback', [], [
|
||||||
|
'Host' => 'mobile-app.local',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips host validation for webhook endpoints', function () {
|
||||||
|
// All webhook routes are under /webhooks/* prefix (see RouteServiceProvider)
|
||||||
|
// and use cryptographic signature validation instead of host validation
|
||||||
|
|
||||||
|
// Test GitHub webhook
|
||||||
|
$response = $this->post('/webhooks/source/github/events', [], [
|
||||||
|
'Host' => 'github-webhook-proxy.local',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
|
||||||
|
// Test GitLab webhook
|
||||||
|
$response = $this->post('/webhooks/source/gitlab/events/manual', [], [
|
||||||
|
'Host' => 'gitlab.example.com',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
|
||||||
|
// Test Stripe webhook
|
||||||
|
$response = $this->post('/webhooks/payments/stripe/events', [], [
|
||||||
|
'Host' => 'stripe-webhook-forwarder.local',
|
||||||
|
]);
|
||||||
|
expect($response->status())->not->toBe(400);
|
||||||
|
});
|
||||||
|
|
|
||||||
63
tests/Unit/ApplicationDeploymentEmptyEnvTest.php
Normal file
63
tests/Unit/ApplicationDeploymentEmptyEnvTest.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to verify that empty .env files are created for build packs that require them.
|
||||||
|
*
|
||||||
|
* This test verifies the fix for the issue where deploying a Docker image without
|
||||||
|
* environment variables would fail because Docker Compose expects a .env file
|
||||||
|
* when env_file: ['.env'] is specified in the compose file.
|
||||||
|
*
|
||||||
|
* The fix ensures that for 'dockerimage' and 'dockercompose' build packs,
|
||||||
|
* an empty .env file is created even when there are no environment variables defined.
|
||||||
|
*/
|
||||||
|
it('determines which build packs require empty .env file creation', function () {
|
||||||
|
// Build packs that set env_file: ['.env'] in the generated compose file
|
||||||
|
// and thus require an empty .env file even when no environment variables are defined
|
||||||
|
$buildPacksRequiringEnvFile = ['dockerimage', 'dockercompose'];
|
||||||
|
|
||||||
|
// Build packs that don't use env_file in the compose file
|
||||||
|
$buildPacksNotRequiringEnvFile = ['dockerfile', 'nixpacks', 'static'];
|
||||||
|
|
||||||
|
foreach ($buildPacksRequiringEnvFile as $buildPack) {
|
||||||
|
// Verify the condition matches our fix
|
||||||
|
$requiresEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||||
|
expect($requiresEnvFile)->toBeTrue("Build pack '{$buildPack}' should require empty .env file");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($buildPacksNotRequiringEnvFile as $buildPack) {
|
||||||
|
// These build packs also use env_file but call save_runtime_environment_variables()
|
||||||
|
// after generate_compose_file(), so they handle empty env files themselves
|
||||||
|
$requiresEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||||
|
expect($requiresEnvFile)->toBeFalse("Build pack '{$buildPack}' should not match the condition");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies dockerimage build pack is included in empty env file creation logic', function () {
|
||||||
|
$buildPack = 'dockerimage';
|
||||||
|
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||||
|
|
||||||
|
expect($shouldCreateEmptyEnvFile)->toBeTrue(
|
||||||
|
'dockerimage build pack should create empty .env file when no environment variables are defined'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies dockercompose build pack is included in empty env file creation logic', function () {
|
||||||
|
$buildPack = 'dockercompose';
|
||||||
|
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||||
|
|
||||||
|
expect($shouldCreateEmptyEnvFile)->toBeTrue(
|
||||||
|
'dockercompose build pack should create empty .env file when no environment variables are defined'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verifies other build packs are not included in empty env file creation logic', function () {
|
||||||
|
$otherBuildPacks = ['dockerfile', 'nixpacks', 'static', 'buildpack'];
|
||||||
|
|
||||||
|
foreach ($otherBuildPacks as $buildPack) {
|
||||||
|
$shouldCreateEmptyEnvFile = ($buildPack === 'dockercompose' || $buildPack === 'dockerimage');
|
||||||
|
|
||||||
|
expect($shouldCreateEmptyEnvFile)->toBeFalse(
|
||||||
|
"Build pack '{$buildPack}' should not create empty .env file in save_runtime_environment_variables()"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"coolify": {
|
"coolify": {
|
||||||
"v4": {
|
"v4": {
|
||||||
"version": "4.0.0-beta.436"
|
"version": "4.0.0-beta.438"
|
||||||
},
|
},
|
||||||
"nightly": {
|
"nightly": {
|
||||||
"version": "4.0.0-beta.437"
|
"version": "4.0.0-beta.439"
|
||||||
},
|
},
|
||||||
"helper": {
|
"helper": {
|
||||||
"version": "1.0.11"
|
"version": "1.0.11"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue