mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
Merge branch 'next' into next
This commit is contained in:
commit
cea4f4a49a
130 changed files with 10849 additions and 806 deletions
|
|
@ -23,19 +23,28 @@ Activate this skill when:
|
|||
|
||||
Use `search-docs` for detailed Pest 4 patterns and documentation.
|
||||
|
||||
## Basic Usage
|
||||
## Test Directory Structure
|
||||
|
||||
### Creating Tests
|
||||
- `tests/Feature/` and `tests/Unit/` — Legacy tests (keep, don't delete)
|
||||
- `tests/v4/Feature/` — New feature tests (SQLite :memory: database)
|
||||
- `tests/v4/Browser/` — Browser tests (Pest Browser Plugin + Playwright)
|
||||
- `tests/Browser/` — Legacy Dusk browser tests (keep, don't delete)
|
||||
|
||||
All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
|
||||
New tests go in `tests/v4/`. The v4 suite uses SQLite :memory: with a schema dump (`database/schema/testing-schema.sql`) instead of running migrations.
|
||||
|
||||
### Test Organization
|
||||
Do NOT remove tests without approval.
|
||||
|
||||
- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
|
||||
- Browser tests: `tests/Browser/` directory.
|
||||
- Do NOT remove tests without approval - these are core application code.
|
||||
## Running Tests
|
||||
|
||||
### Basic Test Structure
|
||||
- All v4 tests: `php artisan test --compact tests/v4/`
|
||||
- Browser tests: `php artisan test --compact tests/v4/Browser/`
|
||||
- Feature tests: `php artisan test --compact tests/v4/Feature/`
|
||||
- Specific file: `php artisan test --compact tests/v4/Browser/LoginTest.php`
|
||||
- Filter: `php artisan test --compact --filter=testName`
|
||||
- Headed (see browser): `./vendor/bin/pest tests/v4/Browser/ --headed`
|
||||
- Debug (pause on failure): `./vendor/bin/pest tests/v4/Browser/ --debug`
|
||||
|
||||
## Basic Test Structure
|
||||
|
||||
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||
|
||||
|
|
@ -45,24 +54,10 @@ it('is true', function () {
|
|||
|
||||
</code-snippet>
|
||||
|
||||
### Running Tests
|
||||
|
||||
- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
|
||||
- Run all tests: `php artisan test --compact`.
|
||||
- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
|
||||
## Assertions
|
||||
|
||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
||||
|
||||
<code-snippet name="Pest Response Assertion" lang="php">
|
||||
|
||||
it('returns all', function () {
|
||||
$this->postJson('/api/docs', [])->assertSuccessful();
|
||||
});
|
||||
|
||||
</code-snippet>
|
||||
|
||||
| Use | Instead of |
|
||||
|-----|------------|
|
||||
| `assertSuccessful()` | `assertStatus(200)` |
|
||||
|
|
@ -75,7 +70,7 @@ Import mock function before use: `use function Pest\Laravel\mock;`
|
|||
|
||||
## Datasets
|
||||
|
||||
Use datasets for repetitive tests (validation rules, etc.):
|
||||
Use datasets for repetitive tests:
|
||||
|
||||
<code-snippet name="Pest Dataset Example" lang="php">
|
||||
|
||||
|
|
@ -88,73 +83,94 @@ it('has emails', function (string $email) {
|
|||
|
||||
</code-snippet>
|
||||
|
||||
## Pest 4 Features
|
||||
## Browser Testing (Pest Browser Plugin + Playwright)
|
||||
|
||||
| Feature | Purpose |
|
||||
|---------|---------|
|
||||
| Browser Testing | Full integration tests in real browsers |
|
||||
| Smoke Testing | Validate multiple pages quickly |
|
||||
| Visual Regression | Compare screenshots for visual changes |
|
||||
| Test Sharding | Parallel CI runs |
|
||||
| Architecture Testing | Enforce code conventions |
|
||||
Browser tests use `pestphp/pest-plugin-browser` with Playwright. They run **outside Docker** — the plugin starts an in-process HTTP server and Playwright browser automatically.
|
||||
|
||||
### Browser Test Example
|
||||
### Key Rules
|
||||
|
||||
Browser tests run in real browsers for full integration testing:
|
||||
1. **Always use `RefreshDatabase`** — the in-process server uses SQLite :memory:
|
||||
2. **Always seed `InstanceSettings::create(['id' => 0])` in `beforeEach`** — most pages crash without it
|
||||
3. **Use `User::factory()` for auth tests** — create users with `id => 0` for root user
|
||||
4. **No Dusk, no Selenium** — use `visit()`, `fill()`, `click()`, `assertSee()` from the Pest Browser API
|
||||
5. **Place tests in `tests/v4/Browser/`**
|
||||
6. **Views with bare `function` declarations** will crash on the second request in the same process — wrap with `function_exists()` guard if you encounter this
|
||||
|
||||
- Browser tests live in `tests/Browser/`.
|
||||
- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
|
||||
- Use `RefreshDatabase` for clean state per test.
|
||||
- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
|
||||
- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
|
||||
- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
|
||||
- Switch color schemes (light/dark mode) when appropriate.
|
||||
- Take screenshots or pause tests for debugging.
|
||||
### Browser Test Template
|
||||
|
||||
<code-snippet name="Pest Browser Test Example" lang="php">
|
||||
<code-snippet name="Coolify Browser Test Template" lang="php">
|
||||
<?php
|
||||
|
||||
it('may reset the password', function () {
|
||||
Notification::fake();
|
||||
use App\Models\InstanceSettings;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
$this->actingAs(User::factory()->create());
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
$page = visit('/sign-in');
|
||||
|
||||
$page->assertSee('Sign In')
|
||||
->assertNoJavaScriptErrors()
|
||||
->click('Forgot Password?')
|
||||
->fill('email', 'nuno@laravel.com')
|
||||
->click('Send Reset Link')
|
||||
->assertSee('We have emailed your password reset link!');
|
||||
|
||||
Notification::assertSent(ResetPassword::class);
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0]);
|
||||
});
|
||||
|
||||
it('can visit the page', function () {
|
||||
$page = visit('/login');
|
||||
|
||||
$page->assertSee('Login');
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Smoke Testing
|
||||
### Browser Test with Form Interaction
|
||||
|
||||
Quickly validate multiple pages have no JavaScript errors:
|
||||
<code-snippet name="Browser Test Form Example" lang="php">
|
||||
it('fails login with invalid credentials', function () {
|
||||
User::factory()->create([
|
||||
'id' => 0,
|
||||
'email' => 'test@example.com',
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
<code-snippet name="Pest Smoke Testing Example" lang="php">
|
||||
|
||||
$pages = visit(['/', '/about', '/contact']);
|
||||
|
||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
||||
$page = visit('/login');
|
||||
|
||||
$page->fill('email', 'random@email.com')
|
||||
->fill('password', 'wrongpassword123')
|
||||
->click('Login')
|
||||
->assertSee('These credentials do not match our records');
|
||||
});
|
||||
</code-snippet>
|
||||
|
||||
### Visual Regression Testing
|
||||
### Browser API Reference
|
||||
|
||||
Capture and compare screenshots to detect visual changes.
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `visit('/path')` | Navigate to a page |
|
||||
| `->fill('field', 'value')` | Fill an input by name |
|
||||
| `->click('Button Text')` | Click a button/link by text |
|
||||
| `->assertSee('text')` | Assert visible text |
|
||||
| `->assertDontSee('text')` | Assert text is not visible |
|
||||
| `->assertPathIs('/path')` | Assert current URL path |
|
||||
| `->assertSeeIn('.selector', 'text')` | Assert text in element |
|
||||
| `->screenshot()` | Capture screenshot |
|
||||
| `->debug()` | Pause test, keep browser open |
|
||||
| `->wait(seconds)` | Wait N seconds |
|
||||
|
||||
### Test Sharding
|
||||
### Debugging
|
||||
|
||||
Split tests across parallel processes for faster CI runs.
|
||||
- Screenshots auto-saved to `tests/Browser/Screenshots/` on failure
|
||||
- `->debug()` pauses and keeps browser open (press Enter to continue)
|
||||
- `->screenshot()` captures state at any point
|
||||
- `--headed` flag shows browser, `--debug` pauses on failure
|
||||
|
||||
### Architecture Testing
|
||||
## SQLite Testing Setup
|
||||
|
||||
Pest 4 includes architecture testing (from Pest 3):
|
||||
v4 tests use SQLite :memory: instead of PostgreSQL. Schema loaded from `database/schema/testing-schema.sql`.
|
||||
|
||||
### Regenerating the Schema
|
||||
|
||||
When migrations change, regenerate from the running PostgreSQL database:
|
||||
|
||||
```bash
|
||||
docker exec coolify php artisan schema:generate-testing
|
||||
```
|
||||
|
||||
## Architecture Testing
|
||||
|
||||
<code-snippet name="Architecture Test Example" lang="php">
|
||||
|
||||
|
|
@ -171,4 +187,7 @@ arch('controllers')
|
|||
- Using `assertStatus(200)` instead of `assertSuccessful()`
|
||||
- Forgetting datasets for repetitive validation tests
|
||||
- Deleting tests without approval
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- Forgetting `assertNoJavaScriptErrors()` in browser tests
|
||||
- **Browser tests: forgetting `InstanceSettings::create(['id' => 0])` — most pages crash without it**
|
||||
- **Browser tests: forgetting `RefreshDatabase` — SQLite :memory: starts empty**
|
||||
- **Browser tests: views with bare `function` declarations crash on second request — wrap with `function_exists()` guard**
|
||||
|
|
|
|||
15
.env.testing
Normal file
15
.env.testing
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
APP_ENV=testing
|
||||
APP_KEY=base64:8VEfVNVkXQ9mH2L33WBWNMF4eQ0BWD5CTzB8mIxcl+k=
|
||||
APP_DEBUG=true
|
||||
|
||||
DB_CONNECTION=testing
|
||||
|
||||
CACHE_DRIVER=array
|
||||
SESSION_DRIVER=array
|
||||
QUEUE_CONNECTION=sync
|
||||
MAIL_MAILER=array
|
||||
TELESCOPE_ENABLED=false
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
|
||||
SELF_HOSTED=true
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
name: Remove Labels and Assignees on Issue Close
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [closed]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
remove-labels-and-assignees:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove labels and assignees
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) {
|
||||
try {
|
||||
if (isFromPR && prBaseBranch !== 'v4.x') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
const labelsToKeep = currentLabels
|
||||
.filter(label => label.name === '⏱︎ Stale')
|
||||
.map(label => label.name);
|
||||
|
||||
await github.rest.issues.setLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
labels: labelsToKeep
|
||||
});
|
||||
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
if (issue.assignees && issue.assignees.length > 0) {
|
||||
await github.rest.issues.removeAssignees({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issueNumber,
|
||||
assignees: issue.assignees.map(assignee => assignee.login)
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
console.error(`Error processing issue ${issueNumber}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.eventName === 'issues') {
|
||||
await processIssue(context.payload.issue.number);
|
||||
}
|
||||
|
||||
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
|
||||
const pr = context.payload.pull_request;
|
||||
await processIssue(pr.number);
|
||||
if (pr.merged && pr.base.ref === 'v4.x' && pr.body) {
|
||||
const issueReferences = pr.body.match(/#(\d+)/g);
|
||||
if (issueReferences) {
|
||||
for (const reference of issueReferences) {
|
||||
const issueNumber = parseInt(reference.substring(1));
|
||||
await processIssue(issueNumber, true, pr.base.ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ on:
|
|||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
|
|
|
|||
1
.github/workflows/coolify-staging-build.yml
vendored
1
.github/workflows/coolify-staging-build.yml
vendored
|
|
@ -11,6 +11,7 @@ on:
|
|||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
- docker/coolify-helper/Dockerfile
|
||||
- docker/coolify-realtime/Dockerfile
|
||||
- docker/testing-host/Dockerfile
|
||||
|
|
|
|||
6
.github/workflows/generate-changelog.yml
vendored
6
.github/workflows/generate-changelog.yml
vendored
|
|
@ -3,6 +3,12 @@ name: Generate Changelog
|
|||
on:
|
||||
push:
|
||||
branches: [ v4.x ]
|
||||
paths-ignore:
|
||||
- .github/workflows/coolify-helper.yml
|
||||
- .github/workflows/coolify-helper-next.yml
|
||||
- .github/workflows/coolify-realtime.yml
|
||||
- .github/workflows/coolify-realtime-next.yml
|
||||
- .github/workflows/pr-quality.yaml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
|
|||
96
.github/workflows/pr-quality.yaml
vendored
Normal file
96
.github/workflows/pr-quality.yaml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
name: PR Quality
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
pr-quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peakoss/anti-slop@v0
|
||||
with:
|
||||
# General Settings
|
||||
max-failures: 3
|
||||
|
||||
# PR Branch Checks
|
||||
allowed-target-branches: "next"
|
||||
blocked-target-branches: ""
|
||||
allowed-source-branches: ""
|
||||
blocked-source-branches: |
|
||||
main
|
||||
master
|
||||
v4.x
|
||||
next
|
||||
|
||||
# PR Quality Checks
|
||||
max-negative-reactions: 0
|
||||
require-maintainer-can-modify: true
|
||||
|
||||
# PR Title Checks
|
||||
require-conventional-title: true
|
||||
|
||||
# PR Description Checks
|
||||
require-description: true
|
||||
max-description-length: 0
|
||||
max-emoji-count: 2
|
||||
require-pr-template: true
|
||||
require-linked-issue: false
|
||||
blocked-terms: "STRAWBERRY"
|
||||
blocked-issue-numbers: 8154
|
||||
|
||||
# Commit Message Checks
|
||||
require-conventional-commits: false
|
||||
blocked-commit-authors: "claude,copilot"
|
||||
|
||||
# File Checks
|
||||
allowed-file-extensions: ""
|
||||
allowed-paths: ""
|
||||
blocked-paths: |
|
||||
README.md
|
||||
SECURITY.md
|
||||
LICENSE
|
||||
CODE_OF_CONDUCT.md
|
||||
templates/service-templates-latest.json
|
||||
templates/service-templates.json
|
||||
require-final-newline: true
|
||||
|
||||
# User Health Checks
|
||||
min-repo-merged-prs: 0
|
||||
min-repo-merge-ratio: 0
|
||||
min-global-merge-ratio: 30
|
||||
global-merge-ratio-exclude-own: false
|
||||
min-account-age: 10
|
||||
|
||||
# Exemptions
|
||||
exempt-author-association: "OWNER,MEMBER,COLLABORATOR"
|
||||
exempt-users: ""
|
||||
exempt-bots: |
|
||||
actions-user
|
||||
dependabot[bot]
|
||||
renovate[bot]
|
||||
github-actions[bot]
|
||||
exempt-draft-prs: false
|
||||
exempt-label: "quality/exempt"
|
||||
exempt-pr-label: ""
|
||||
exempt-milestones: ""
|
||||
exempt-pr-milestones: ""
|
||||
exempt-all-milestones: false
|
||||
exempt-all-pr-milestones: false
|
||||
|
||||
# PR Success Actions
|
||||
success-add-pr-labels: "quality/verified"
|
||||
|
||||
# PR Failure Actions
|
||||
close-pr: true
|
||||
lock-pr: false
|
||||
delete-branch: false
|
||||
failure-pr-message: "This PR did not pass quality checks so it will be closed. If you believe this is a mistake please let us know."
|
||||
failure-remove-pr-labels: ""
|
||||
failure-remove-all-pr-labels: true
|
||||
failure-add-pr-labels: "quality/rejected"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -38,3 +38,5 @@ docker/coolify-realtime/node_modules
|
|||
.DS_Store
|
||||
CHANGELOG.md
|
||||
/.workspaces
|
||||
tests/Browser/Screenshots
|
||||
tests/v4/Browser/Screenshots
|
||||
|
|
|
|||
|
|
@ -112,12 +112,52 @@ class StartDatabaseProxy
|
|||
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
||||
$nginxconf_base64 = base64_encode($nginxconf);
|
||||
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
|
||||
instant_remote_process([
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up -d",
|
||||
], $server);
|
||||
|
||||
try {
|
||||
instant_remote_process([
|
||||
"mkdir -p $configuration_dir",
|
||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up -d",
|
||||
], $server);
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($this->isNonTransientError($e->getMessage())) {
|
||||
$database->update(['is_public' => false]);
|
||||
|
||||
$team = data_get($database, 'environment.project.team')
|
||||
?? data_get($database, 'service.environment.project.team');
|
||||
|
||||
$team?->notify(
|
||||
new \App\Notifications\Container\ContainerRestarted(
|
||||
"TCP Proxy for {$database->name} database has been disabled due to error: {$e->getMessage()}",
|
||||
$server,
|
||||
)
|
||||
);
|
||||
|
||||
ray("Database proxy for {$database->name} disabled due to non-transient error: {$e->getMessage()}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function isNonTransientError(string $message): bool
|
||||
{
|
||||
$nonTransientPatterns = [
|
||||
'port is already allocated',
|
||||
'address already in use',
|
||||
'Bind for',
|
||||
];
|
||||
|
||||
foreach ($nonTransientPatterns as $pattern) {
|
||||
if (str_contains($message, $pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,9 @@ class StartKeydb
|
|||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
if (! is_null($this->database->keydb_conf) && ! empty($this->database->keydb_conf)) {
|
||||
$this->commands[] = "chown 999:999 $this->configuration_dir/keydb.conf";
|
||||
}
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
|
|
|
|||
|
|
@ -204,6 +204,9 @@ class StartRedis
|
|||
if ($this->database->enable_ssl) {
|
||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
||||
}
|
||||
if (! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf)) {
|
||||
$this->commands[] = "chown 999:999 $this->configuration_dir/redis.conf";
|
||||
}
|
||||
$this->commands[] = "docker stop -t 10 $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker rm -f $container_name 2>/dev/null || true";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ class InstallDocker
|
|||
);
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$base64Cert = base64_encode($serverCert->ssl_certificate);
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$serverCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
remote_process($commands, $server);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ class OpenApi extends Command
|
|||
echo $process->output();
|
||||
|
||||
$yaml = file_get_contents('openapi.yaml');
|
||||
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT);
|
||||
|
||||
$json = json_encode(Yaml::parse($yaml), JSON_PRETTY_PRINT)."\n";
|
||||
file_put_contents('openapi.json', $json);
|
||||
echo "Converted OpenAPI YAML to JSON.\n";
|
||||
}
|
||||
|
|
|
|||
222
app/Console/Commands/GenerateTestingSchema.php
Normal file
222
app/Console/Commands/GenerateTestingSchema.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class GenerateTestingSchema extends Command
|
||||
{
|
||||
protected $signature = 'schema:generate-testing {--connection=pgsql : The database connection to read from}';
|
||||
|
||||
protected $description = 'Generate SQLite testing schema from the PostgreSQL database';
|
||||
|
||||
private array $typeMap = [
|
||||
'/\bbigint\b/' => 'INTEGER',
|
||||
'/\binteger\b/' => 'INTEGER',
|
||||
'/\bsmallint\b/' => 'INTEGER',
|
||||
'/\bboolean\b/' => 'INTEGER',
|
||||
'/character varying\(\d+\)/' => 'TEXT',
|
||||
'/timestamp\(\d+\) without time zone/' => 'TEXT',
|
||||
'/timestamp\(\d+\) with time zone/' => 'TEXT',
|
||||
'/\bjsonb\b/' => 'TEXT',
|
||||
'/\bjson\b/' => 'TEXT',
|
||||
'/\buuid\b/' => 'TEXT',
|
||||
'/double precision/' => 'REAL',
|
||||
'/numeric\(\d+,\d+\)/' => 'REAL',
|
||||
'/\bdate\b/' => 'TEXT',
|
||||
];
|
||||
|
||||
private array $castRemovals = [
|
||||
'::character varying',
|
||||
'::text',
|
||||
'::integer',
|
||||
'::boolean',
|
||||
'::timestamp without time zone',
|
||||
'::timestamp with time zone',
|
||||
'::numeric',
|
||||
];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$connection = $this->option('connection');
|
||||
|
||||
if (DB::connection($connection)->getDriverName() !== 'pgsql') {
|
||||
$this->error("Connection '{$connection}' is not PostgreSQL.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Reading schema from PostgreSQL...');
|
||||
|
||||
$tables = $this->getTables($connection);
|
||||
$lastMigration = DB::connection($connection)
|
||||
->table('migrations')
|
||||
->orderByDesc('id')
|
||||
->value('migration');
|
||||
|
||||
$output = [];
|
||||
$output[] = '-- Generated by: php artisan schema:generate-testing';
|
||||
$output[] = '-- Date: '.now()->format('Y-m-d H:i:s');
|
||||
$output[] = '-- Last migration: '.($lastMigration ?? 'none');
|
||||
$output[] = '';
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$columns = $this->getColumns($connection, $table);
|
||||
$output[] = $this->generateCreateTable($table, $columns);
|
||||
}
|
||||
|
||||
$indexes = $this->getIndexes($connection, $tables);
|
||||
foreach ($indexes as $index) {
|
||||
$output[] = $index;
|
||||
}
|
||||
|
||||
$output[] = '';
|
||||
$output[] = '-- Migration records';
|
||||
|
||||
$migrations = DB::connection($connection)->table('migrations')->orderBy('id')->get();
|
||||
foreach ($migrations as $m) {
|
||||
$migration = str_replace("'", "''", $m->migration);
|
||||
$output[] = "INSERT INTO \"migrations\" (\"id\", \"migration\", \"batch\") VALUES ({$m->id}, '{$migration}', {$m->batch});";
|
||||
}
|
||||
|
||||
$path = database_path('schema/testing-schema.sql');
|
||||
|
||||
if (! is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($path, implode("\n", $output)."\n");
|
||||
|
||||
$this->info("Schema written to {$path}");
|
||||
$this->info(count($tables).' tables, '.count($migrations).' migration records.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function getTables(string $connection): array
|
||||
{
|
||||
return collect(DB::connection($connection)->select(
|
||||
"SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
|
||||
))->pluck('tablename')->toArray();
|
||||
}
|
||||
|
||||
private function getColumns(string $connection, string $table): array
|
||||
{
|
||||
return DB::connection($connection)->select(
|
||||
"SELECT column_name, data_type, character_maximum_length, column_default,
|
||||
is_nullable, udt_name, numeric_precision, numeric_scale, datetime_precision
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = ?
|
||||
ORDER BY ordinal_position",
|
||||
[$table]
|
||||
);
|
||||
}
|
||||
|
||||
private function generateCreateTable(string $table, array $columns): string
|
||||
{
|
||||
$lines = [];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
$lines[] = ' '.$this->generateColumnDef($table, $col);
|
||||
}
|
||||
|
||||
return "CREATE TABLE IF NOT EXISTS \"{$table}\" (\n".implode(",\n", $lines)."\n);\n";
|
||||
}
|
||||
|
||||
private function generateColumnDef(string $table, object $col): string
|
||||
{
|
||||
$name = $col->column_name;
|
||||
$sqliteType = $this->convertType($col);
|
||||
|
||||
// Auto-increment primary key for id columns
|
||||
if ($name === 'id' && $sqliteType === 'INTEGER' && $col->is_nullable === 'NO' && str_contains((string) $col->column_default, 'nextval')) {
|
||||
return "\"{$name}\" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL";
|
||||
}
|
||||
|
||||
$parts = ["\"{$name}\"", $sqliteType];
|
||||
|
||||
// Default value
|
||||
$default = $col->column_default;
|
||||
if ($default !== null && ! str_contains($default, 'nextval')) {
|
||||
$default = $this->cleanDefault($default);
|
||||
$parts[] = "DEFAULT {$default}";
|
||||
}
|
||||
|
||||
// NOT NULL
|
||||
if ($col->is_nullable === 'NO') {
|
||||
$parts[] = 'NOT NULL';
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
private function convertType(object $col): string
|
||||
{
|
||||
$pgType = $col->data_type;
|
||||
|
||||
return match (true) {
|
||||
in_array($pgType, ['bigint', 'integer', 'smallint']) => 'INTEGER',
|
||||
$pgType === 'boolean' => 'INTEGER',
|
||||
in_array($pgType, ['character varying', 'text', 'USER-DEFINED']) => 'TEXT',
|
||||
str_contains($pgType, 'timestamp') => 'TEXT',
|
||||
in_array($pgType, ['json', 'jsonb']) => 'TEXT',
|
||||
$pgType === 'uuid' => 'TEXT',
|
||||
$pgType === 'double precision' => 'REAL',
|
||||
$pgType === 'numeric' => 'REAL',
|
||||
$pgType === 'date' => 'TEXT',
|
||||
default => 'TEXT',
|
||||
};
|
||||
}
|
||||
|
||||
private function cleanDefault(string $default): string
|
||||
{
|
||||
foreach ($this->castRemovals as $cast) {
|
||||
$default = str_replace($cast, '', $default);
|
||||
}
|
||||
|
||||
// Remove array type casts like ::text[]
|
||||
$default = preg_replace('/::[\w\s]+(\[\])?/', '', $default);
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function getIndexes(string $connection, array $tables): array
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$indexes = DB::connection($connection)->select(
|
||||
"SELECT indexname, tablename, indexdef FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY tablename, indexname"
|
||||
);
|
||||
|
||||
foreach ($indexes as $idx) {
|
||||
$def = $idx->indexdef;
|
||||
|
||||
// Skip primary key indexes
|
||||
if (str_contains($def, '_pkey')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip PG-specific indexes (GIN, GIST, expression indexes)
|
||||
if (preg_match('/USING (gin|gist)/i', $def)) {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($def, '->>') || str_contains($def, '::')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert to SQLite-compatible CREATE INDEX
|
||||
$unique = str_contains($def, 'UNIQUE') ? 'UNIQUE ' : '';
|
||||
|
||||
// Extract columns from the index definition
|
||||
if (preg_match('/\((.+)\)$/', $def, $m)) {
|
||||
$cols = $m[1];
|
||||
$results[] = "CREATE {$unique}INDEX IF NOT EXISTS \"{$idx->indexname}\" ON \"{$idx->tablename}\" ({$cols});";
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ class Kernel extends ConsoleKernel
|
|||
}
|
||||
|
||||
// $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||
$this->scheduleInstance->command('cleanup:redis')->weekly();
|
||||
$this->scheduleInstance->command('cleanup:redis --clear-locks')->daily();
|
||||
|
||||
if (isDev()) {
|
||||
// Instance Jobs
|
||||
|
|
|
|||
|
|
@ -1002,7 +1002,7 @@ class ApplicationsController extends Controller
|
|||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
|
||||
$allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'dockerfile_location', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server', 'static_image', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'autogenerate_domain', 'is_container_label_escape_enabled'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
|
|
@ -1101,7 +1101,6 @@ class ApplicationsController extends Controller
|
|||
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||
'docker_compose_location' => 'string',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain',
|
||||
'docker_compose_domains.*.name' => 'string|required',
|
||||
|
|
@ -1297,7 +1296,6 @@ class ApplicationsController extends Controller
|
|||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||
'github_app_uuid' => 'string|required',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_location' => 'string',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain',
|
||||
'docker_compose_domains.*.name' => 'string|required',
|
||||
|
|
@ -1525,7 +1523,6 @@ class ApplicationsController extends Controller
|
|||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||
'private_key_uuid' => 'string|required',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_location' => 'string',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain',
|
||||
'docker_compose_domains.*.name' => 'string|required',
|
||||
|
|
@ -2463,14 +2460,13 @@ class ApplicationsController extends Controller
|
|||
$this->authorize('update', $application);
|
||||
|
||||
$server = $application->destination->server;
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
$allowedFields = ['name', 'description', 'is_static', 'is_spa', 'is_auto_deploy_enabled', 'is_force_https_enabled', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'custom_network_aliases', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_type', 'health_check_command', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'dockerfile_location', 'docker_compose_location', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server', 'custom_nginx_configuration', 'is_http_basic_auth_enabled', 'http_basic_auth_username', 'http_basic_auth_password', 'connect_to_docker_network', 'force_domain_override', 'is_container_label_escape_enabled'];
|
||||
|
||||
$validationRules = [
|
||||
'name' => 'string|max:255',
|
||||
'description' => 'string|nullable',
|
||||
'static_image' => 'string',
|
||||
'watch_paths' => 'string|nullable',
|
||||
'docker_compose_location' => 'string',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_domains.*' => 'array:name,domain',
|
||||
'docker_compose_domains.*.name' => 'string|required',
|
||||
|
|
|
|||
|
|
@ -127,6 +127,10 @@ class DeployController extends Controller
|
|||
if (! $deployment) {
|
||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
||||
}
|
||||
$application = $deployment->application;
|
||||
if (! $application || data_get($application->team(), 'id') !== $teamId) {
|
||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json($this->removeSensitiveData($deployment));
|
||||
}
|
||||
|
|
|
|||
922
app/Http/Controllers/Api/ScheduledTasksController.php
Normal file
922
app/Http/Controllers/Api/ScheduledTasksController.php
Normal file
|
|
@ -0,0 +1,922 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Application;
|
||||
use App\Models\ScheduledTask;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
class ScheduledTasksController extends Controller
|
||||
{
|
||||
private function removeSensitiveData($task)
|
||||
{
|
||||
$task->makeHidden([
|
||||
'id',
|
||||
'team_id',
|
||||
'application_id',
|
||||
'service_id',
|
||||
]);
|
||||
|
||||
return serializeApiResponse($task);
|
||||
}
|
||||
|
||||
private function resolveApplication(Request $request, int $teamId): ?Application
|
||||
{
|
||||
return Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
|
||||
}
|
||||
|
||||
private function resolveService(Request $request, int $teamId): ?Service
|
||||
{
|
||||
return Service::whereRelation('environment.project.team', 'id', $teamId)->where('uuid', $request->uuid)->first();
|
||||
}
|
||||
|
||||
private function listTasks(Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
$tasks = $resource->scheduled_tasks->map(function ($task) {
|
||||
return $this->removeSensitiveData($task);
|
||||
});
|
||||
|
||||
return response()->json($tasks);
|
||||
}
|
||||
|
||||
private function createTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$allowedFields = ['name', 'command', 'frequency', 'container', 'timeout', 'enabled'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'command' => 'required|string',
|
||||
'frequency' => 'required|string',
|
||||
'container' => 'string|nullable',
|
||||
'timeout' => 'integer|min:1',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (! validate_cron_expression($request->frequency)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['frequency' => ['Invalid cron expression or frequency format.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
|
||||
$task = new ScheduledTask;
|
||||
$task->name = $request->name;
|
||||
$task->command = $request->command;
|
||||
$task->frequency = $request->frequency;
|
||||
$task->container = $request->container;
|
||||
$task->timeout = $request->has('timeout') ? $request->timeout : 300;
|
||||
$task->enabled = $request->has('enabled') ? $request->enabled : true;
|
||||
$task->team_id = $teamId;
|
||||
|
||||
if ($resource instanceof Application) {
|
||||
$task->application_id = $resource->id;
|
||||
} elseif ($resource instanceof Service) {
|
||||
$task->service_id = $resource->id;
|
||||
}
|
||||
|
||||
$task->save();
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 201);
|
||||
}
|
||||
|
||||
private function updateTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ($request->all() === []) {
|
||||
return response()->json(['message' => 'At least one field must be provided.'], 422);
|
||||
}
|
||||
|
||||
$allowedFields = ['name', 'command', 'frequency', 'container', 'timeout', 'enabled'];
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'name' => 'string|max:255',
|
||||
'command' => 'string',
|
||||
'frequency' => 'string',
|
||||
'container' => 'string|nullable',
|
||||
'timeout' => 'integer|min:1',
|
||||
'enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
if ($request->has('frequency') && ! validate_cron_expression($request->frequency)) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['frequency' => ['Invalid cron expression or frequency format.']],
|
||||
], 422);
|
||||
}
|
||||
|
||||
$task = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->first();
|
||||
if (! $task) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
$task->update($request->only($allowedFields));
|
||||
|
||||
return response()->json($this->removeSensitiveData($task), 200);
|
||||
}
|
||||
|
||||
private function deleteTask(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
$deleted = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->delete();
|
||||
if (! $deleted) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Scheduled task deleted.']);
|
||||
}
|
||||
|
||||
private function getExecutions(Request $request, Application|Service $resource): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resource);
|
||||
|
||||
$task = $resource->scheduled_tasks()->where('uuid', $request->task_uuid)->first();
|
||||
if (! $task) {
|
||||
return response()->json(['message' => 'Scheduled task not found.'], 404);
|
||||
}
|
||||
|
||||
$executions = $task->executions()->get()->map(function ($execution) {
|
||||
$execution->makeHidden(['id', 'scheduled_task_id']);
|
||||
|
||||
return serializeApiResponse($execution);
|
||||
});
|
||||
|
||||
return response()->json($executions);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Tasks',
|
||||
description: 'List all scheduled tasks for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks',
|
||||
operationId: 'list-scheduled-tasks-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all scheduled tasks for an application.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTask')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->listTasks($application);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Task',
|
||||
description: 'Create a new scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks',
|
||||
operationId: 'create-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['name', 'command', 'frequency'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Scheduled task created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->createTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Task',
|
||||
description: 'Update a scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'update-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->updateTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Task',
|
||||
description: 'Delete a scheduled task for an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'delete-scheduled-task-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Scheduled task deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->deleteTask($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Executions',
|
||||
description: 'List all executions for a scheduled task on an application.',
|
||||
path: '/applications/{uuid}/scheduled-tasks/{task_uuid}/executions',
|
||||
operationId: 'list-scheduled-task-executions-by-application-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the application.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all executions for a scheduled task.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTaskExecution')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_application_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$application = $this->resolveApplication($request, $teamId);
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->getExecutions($request, $application);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Tasks',
|
||||
description: 'List all scheduled tasks for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks',
|
||||
operationId: 'list-scheduled-tasks-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all scheduled tasks for a service.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTask')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function scheduled_tasks_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->listTasks($service);
|
||||
}
|
||||
|
||||
#[OA\Post(
|
||||
summary: 'Create Task',
|
||||
description: 'Create a new scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks',
|
||||
operationId: 'create-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
required: ['name', 'command', 'frequency'],
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 201,
|
||||
description: 'Scheduled task created.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function create_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->createTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Patch(
|
||||
summary: 'Update Task',
|
||||
description: 'Update a scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'update-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
requestBody: new OA\RequestBody(
|
||||
description: 'Scheduled task data',
|
||||
required: true,
|
||||
content: new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.', 'default' => 300],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.', 'default' => true],
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task updated.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(ref: '#/components/schemas/ScheduledTask')
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 422,
|
||||
ref: '#/components/responses/422',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function update_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->updateTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Delete(
|
||||
summary: 'Delete Task',
|
||||
description: 'Delete a scheduled task for a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}',
|
||||
operationId: 'delete-scheduled-task-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Scheduled task deleted.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'object',
|
||||
properties: [
|
||||
'message' => ['type' => 'string', 'example' => 'Scheduled task deleted.'],
|
||||
]
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function delete_scheduled_task_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->deleteTask($request, $service);
|
||||
}
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'List Executions',
|
||||
description: 'List all executions for a scheduled task on a service.',
|
||||
path: '/services/{uuid}/scheduled-tasks/{task_uuid}/executions',
|
||||
operationId: 'list-scheduled-task-executions-by-service-uuid',
|
||||
security: [
|
||||
['bearerAuth' => []],
|
||||
],
|
||||
tags: ['Scheduled Tasks'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the service.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'task_uuid',
|
||||
in: 'path',
|
||||
description: 'UUID of the scheduled task.',
|
||||
required: true,
|
||||
schema: new OA\Schema(
|
||||
type: 'string',
|
||||
)
|
||||
),
|
||||
],
|
||||
responses: [
|
||||
new OA\Response(
|
||||
response: 200,
|
||||
description: 'Get all executions for a scheduled task.',
|
||||
content: [
|
||||
new OA\MediaType(
|
||||
mediaType: 'application/json',
|
||||
schema: new OA\Schema(
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/ScheduledTaskExecution')
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
new OA\Response(
|
||||
response: 401,
|
||||
ref: '#/components/responses/401',
|
||||
),
|
||||
new OA\Response(
|
||||
response: 404,
|
||||
ref: '#/components/responses/404',
|
||||
),
|
||||
]
|
||||
)]
|
||||
public function executions_by_service_uuid(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$service = $this->resolveService($request, $teamId);
|
||||
if (! $service) {
|
||||
return response()->json(['message' => 'Service not found.'], 404);
|
||||
}
|
||||
|
||||
return $this->getExecutions($request, $service);
|
||||
}
|
||||
}
|
||||
|
|
@ -290,9 +290,12 @@ class ServersController extends Controller
|
|||
}
|
||||
$uuid = $request->get('uuid');
|
||||
if ($uuid) {
|
||||
$domains = Application::getDomainsByUuid($uuid);
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
|
||||
if (! $application) {
|
||||
return response()->json(['message' => 'Application not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(serializeApiResponse($domains));
|
||||
return response()->json(serializeApiResponse($application->fqdns));
|
||||
}
|
||||
$projects = Project::where('team_id', $teamId)->get();
|
||||
$domains = collect();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class CheckForcePasswordReset
|
|||
}
|
||||
$force_password_reset = auth()->user()->force_password_reset;
|
||||
if ($force_password_reset) {
|
||||
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'livewire/update' || $request->path() === 'logout') {
|
||||
if ($request->routeIs('auth.force-password-reset') || $request->path() === 'force-password-reset' || $request->path() === 'two-factor-challenge' || $request->path() === 'livewire/update' || $request->path() === 'logout') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private bool $dockerBuildkitSupported = false;
|
||||
|
||||
private bool $dockerSecretsSupported = false;
|
||||
|
||||
private bool $skip_build = false;
|
||||
|
||||
private Collection|string $build_secrets;
|
||||
|
|
@ -251,7 +253,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
if ($this->application->build_pack === 'dockerfile') {
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
$this->dockerfile_location = $this->application->dockerfile_location;
|
||||
$this->dockerfile_location = $this->validatePathField($this->application->dockerfile_location, 'dockerfile_location');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -381,13 +383,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private function detectBuildKitCapabilities(): void
|
||||
{
|
||||
// If build secrets are not enabled, skip detection and use traditional args
|
||||
if (! $this->application->settings->use_build_secrets) {
|
||||
$this->dockerBuildkitSupported = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$serverToCheck = $this->use_build_server ? $this->build_server : $this->server;
|
||||
$serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})";
|
||||
|
||||
|
|
@ -403,53 +398,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) {
|
||||
$this->dockerBuildkitSupported = false;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+). Build secrets feature disabled.");
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+).");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$buildkitEnabled = instant_remote_process(
|
||||
// Check buildx availability (always installed by Coolify on Docker 24.0+)
|
||||
$buildxAvailable = instant_remote_process(
|
||||
["docker buildx version >/dev/null 2>&1 && echo 'available' || echo 'not-available'"],
|
||||
$serverToCheck
|
||||
);
|
||||
|
||||
if (trim($buildkitEnabled) !== 'available') {
|
||||
if (trim($buildxAvailable) === 'available') {
|
||||
$this->dockerBuildkitSupported = true;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}.");
|
||||
} else {
|
||||
// Fallback: test DOCKER_BUILDKIT=1 support via --progress flag
|
||||
$buildkitTest = instant_remote_process(
|
||||
["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
|
||||
["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q '\\-\\-progress' && echo 'supported' || echo 'not-supported'"],
|
||||
$serverToCheck
|
||||
);
|
||||
|
||||
if (trim($buildkitTest) === 'supported') {
|
||||
$this->dockerBuildkitSupported = true;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}.");
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit support detected on {$serverName}.");
|
||||
} else {
|
||||
$this->dockerBuildkitSupported = false;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support.");
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit. Build output progress will be limited.");
|
||||
}
|
||||
} else {
|
||||
// Buildx is available, which means BuildKit is available
|
||||
// Now specifically test for secrets support
|
||||
}
|
||||
|
||||
// If build secrets are enabled and BuildKit is available, verify --secret flag support
|
||||
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported) {
|
||||
$secretsTest = instant_remote_process(
|
||||
["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
|
||||
$serverToCheck
|
||||
);
|
||||
|
||||
if (trim($secretsTest) === 'supported') {
|
||||
$this->dockerBuildkitSupported = true;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}.");
|
||||
$this->dockerSecretsSupported = true;
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
|
||||
} else {
|
||||
$this->dockerBuildkitSupported = false;
|
||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported.");
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
|
||||
$this->dockerSecretsSupported = false;
|
||||
$this->application_deployment_queue->addLogEntry("Docker on {$serverName} does not support build secrets. Using traditional build arguments.");
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->dockerBuildkitSupported = false;
|
||||
$this->dockerSecretsSupported = false;
|
||||
$this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}");
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but detection failed. Using traditional build arguments.');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -571,7 +568,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
private function deploy_docker_compose_buildpack()
|
||||
{
|
||||
if (data_get($this->application, 'docker_compose_location')) {
|
||||
$this->docker_compose_location = $this->application->docker_compose_location;
|
||||
$this->docker_compose_location = $this->validatePathField($this->application->docker_compose_location, 'docker_compose_location');
|
||||
}
|
||||
if (data_get($this->application, 'docker_compose_custom_start_command')) {
|
||||
$this->docker_compose_custom_start_command = $this->application->docker_compose_custom_start_command;
|
||||
|
|
@ -632,7 +629,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
// For raw compose, we cannot automatically add secrets configuration
|
||||
// User must define it manually in their docker-compose file
|
||||
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
|
||||
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
|
||||
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
||||
}
|
||||
} else {
|
||||
|
|
@ -653,7 +650,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
|
||||
// Add build secrets to compose file if enabled and BuildKit is supported
|
||||
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
|
||||
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
|
||||
$composeFile = $this->add_build_secrets_to_compose($composeFile);
|
||||
}
|
||||
|
||||
|
|
@ -689,8 +686,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
// Inject build arguments after build subcommand if not using build secrets
|
||||
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
|
||||
$build_args_string = $this->build_args->implode(' ');
|
||||
// Escape single quotes for bash -c context used by executeInDocker
|
||||
$build_args_string = str_replace("'", "'\\''", $build_args_string);
|
||||
|
||||
// Inject build args right after 'build' subcommand (not at the end)
|
||||
$original_command = $build_command;
|
||||
|
|
@ -702,9 +697,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
|
||||
);
|
||||
try {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
if (str_contains($e->getMessage(), "matching `'") || str_contains($e->getMessage(), 'unexpected EOF')) {
|
||||
throw new DeploymentException("Custom build command failed due to shell syntax error. Please check your command for special characters (like unmatched quotes): {$this->docker_compose_custom_build_command}");
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$command = "{$this->coolify_variables} docker compose";
|
||||
// Prepend DOCKER_BUILDKIT=1 if BuildKit is supported
|
||||
|
|
@ -721,8 +724,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
|
||||
$build_args_string = $this->build_args->implode(' ');
|
||||
// Escape single quotes for bash -c context used by executeInDocker
|
||||
$build_args_string = str_replace("'", "'\\''", $build_args_string);
|
||||
$command .= " {$build_args_string}";
|
||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
|
||||
}
|
||||
|
|
@ -768,9 +769,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
);
|
||||
|
||||
$this->write_deployment_configurations();
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), 'hidden' => true],
|
||||
);
|
||||
|
||||
try {
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$start_command}"), 'hidden' => true],
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
if (str_contains($e->getMessage(), "matching `'") || str_contains($e->getMessage(), 'unexpected EOF')) {
|
||||
throw new DeploymentException("Custom start command failed due to shell syntax error. Please check your command for special characters (like unmatched quotes): {$this->docker_compose_custom_start_command}");
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$this->write_deployment_configurations();
|
||||
$this->docker_compose_location = '/docker-compose.yaml';
|
||||
|
|
@ -831,7 +841,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->server = $this->build_server;
|
||||
}
|
||||
if (data_get($this->application, 'dockerfile_location')) {
|
||||
$this->dockerfile_location = $this->application->dockerfile_location;
|
||||
$this->dockerfile_location = $this->validatePathField($this->application->dockerfile_location, 'dockerfile_location');
|
||||
}
|
||||
$this->prepare_builder_image();
|
||||
$this->check_git_if_build_needed();
|
||||
|
|
@ -1800,7 +1810,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$counter = 1;
|
||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
|
||||
$healthcheckLabel = $this->application->health_check_type === 'cmd' ? 'Healthcheck command' : 'Healthcheck URL';
|
||||
$this->application_deployment_queue->addLogEntry("{$healthcheckLabel} (inside the container): {$this->full_healthcheck_url}");
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
||||
$sleeptime = 0;
|
||||
|
|
@ -2758,29 +2769,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private function generate_healthcheck_commands()
|
||||
{
|
||||
// Handle CMD type healthcheck
|
||||
if ($this->application->health_check_type === 'cmd' && ! empty($this->application->health_check_command)) {
|
||||
$this->full_healthcheck_url = $this->application->health_check_command;
|
||||
|
||||
return $this->application->health_check_command;
|
||||
}
|
||||
|
||||
// HTTP type healthcheck (default)
|
||||
if (! $this->application->health_check_port) {
|
||||
$health_check_port = $this->application->ports_exposes_array[0];
|
||||
$health_check_port = (int) $this->application->ports_exposes_array[0];
|
||||
} else {
|
||||
$health_check_port = $this->application->health_check_port;
|
||||
$health_check_port = (int) $this->application->health_check_port;
|
||||
}
|
||||
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
|
||||
$health_check_port = 80;
|
||||
}
|
||||
if ($this->application->health_check_path) {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1",
|
||||
];
|
||||
|
||||
$method = $this->sanitizeHealthCheckValue($this->application->health_check_method, '/^[A-Z]+$/', 'GET');
|
||||
$scheme = $this->sanitizeHealthCheckValue($this->application->health_check_scheme, '/^https?$/', 'http');
|
||||
$host = $this->sanitizeHealthCheckValue($this->application->health_check_host, '/^[a-zA-Z0-9.\-_]+$/', 'localhost');
|
||||
$path = $this->application->health_check_path
|
||||
? $this->sanitizeHealthCheckValue($this->application->health_check_path, '#^[a-zA-Z0-9/\-_.~%]+$#', '/')
|
||||
: null;
|
||||
|
||||
$url = escapeshellarg("{$scheme}://{$host}:{$health_check_port}".($path ?? '/'));
|
||||
$method = escapeshellarg($method);
|
||||
|
||||
if ($path) {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$scheme}://{$host}:{$health_check_port}{$path}";
|
||||
} else {
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1",
|
||||
];
|
||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$scheme}://{$host}:{$health_check_port}/";
|
||||
}
|
||||
|
||||
$generated_healthchecks_commands = [
|
||||
"curl -s -X {$method} -f {$url} > /dev/null || wget -q -O- {$url} > /dev/null || exit 1",
|
||||
];
|
||||
|
||||
return implode(' ', $generated_healthchecks_commands);
|
||||
}
|
||||
|
||||
private function sanitizeHealthCheckValue(string $value, string $pattern, string $default): string
|
||||
{
|
||||
if (preg_match($pattern, $value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function pull_latest_image($image)
|
||||
{
|
||||
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
|
||||
|
|
@ -2817,7 +2854,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
if ($this->dockerBuildkitSupported) {
|
||||
$build_command = "DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
} else {
|
||||
$build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile -t {$this->production_image_name} {$this->workdir}";
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
|
|
@ -2857,21 +2898,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
private function build_image()
|
||||
{
|
||||
// Add Coolify related variables to the build args/secrets
|
||||
if ($this->dockerBuildkitSupported) {
|
||||
// Coolify variables are already included in the secrets from generate_build_env_variables
|
||||
// build_secrets is already a string at this point
|
||||
} else {
|
||||
if (! $this->dockerBuildkitSupported) {
|
||||
// Traditional build args approach - generate COOLIFY_ variables locally
|
||||
// Generate COOLIFY_ variables locally for build args
|
||||
$coolify_envs = $this->generate_coolify_env_variables(forBuildTime: true);
|
||||
$coolify_envs->each(function ($value, $key) {
|
||||
$this->build_args->push("--build-arg '{$key}'");
|
||||
});
|
||||
$this->build_args = $this->build_args instanceof \Illuminate\Support\Collection
|
||||
? $this->build_args->implode(' ')
|
||||
: (string) $this->build_args;
|
||||
}
|
||||
|
||||
// Always convert build_args Collection to string for command interpolation
|
||||
$this->build_args = $this->build_args instanceof \Illuminate\Support\Collection
|
||||
? $this->build_args->implode(' ')
|
||||
: (string) $this->build_args;
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
if ($this->disableBuildCache) {
|
||||
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
|
||||
|
|
@ -2899,7 +2938,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the nixpacks Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
|
|
@ -2907,9 +2946,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
ray($build_command);
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
|
|
@ -2919,18 +2957,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the nixpacks Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->workdir}");
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->build_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2952,7 +2988,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
|
||||
} else {
|
||||
// Dockerfile buildpack
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
|
|
@ -2963,19 +2999,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
}
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} --progress plain -t $this->build_image_name {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
} else {
|
||||
// Traditional build with args
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} --network {$this->destination->network} -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t $this->build_image_name {$this->workdir}");
|
||||
}
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
|
|
@ -3010,7 +3044,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
$nginx_config = base64_encode(defaultNginxConfiguration());
|
||||
}
|
||||
}
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
if ($this->dockerBuildkitSupported) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/Dockerfile {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
|
|
@ -3035,7 +3073,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
} else {
|
||||
// Pure Dockerfile based deployment
|
||||
if ($this->application->dockerfile) {
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
|
|
@ -3044,12 +3082,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
} else {
|
||||
$build_command = "DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}";
|
||||
}
|
||||
} else {
|
||||
// Traditional build with args
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
} else {
|
||||
// Traditional build with args (no --progress for legacy builder compatibility)
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --pull {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
|
||||
}
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
|
|
@ -3079,18 +3124,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the nixpacks Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
|
|
@ -3100,18 +3143,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||
'hidden' => true,
|
||||
]);
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the nixpacks Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
|
|
@ -3132,7 +3173,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
|
||||
} else {
|
||||
// Dockerfile buildpack
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
// Modify the Dockerfile to use build secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||
// Use BuildKit with secrets
|
||||
|
|
@ -3144,19 +3185,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
}
|
||||
} elseif ($this->dockerBuildkitSupported) {
|
||||
// BuildKit without secrets
|
||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location}{$secrets_flags} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("DOCKER_BUILDKIT=1 docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} --progress plain -t {$this->production_image_name} {$this->build_args} {$this->workdir}");
|
||||
}
|
||||
} else {
|
||||
// Traditional build with args
|
||||
if ($this->force_rebuild) {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build --no-cache {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
|
||||
} else {
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}");
|
||||
$build_command = $this->wrap_build_command_with_env_export("docker build {$this->buildTarget} {$this->addHosts} --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} -t {$this->production_image_name} {$this->workdir}");
|
||||
}
|
||||
}
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
|
|
@ -3332,7 +3371,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
$this->analyzeBuildTimeVariables($variables);
|
||||
}
|
||||
|
||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||
if ($this->dockerSecretsSupported) {
|
||||
$this->generate_build_secrets($variables);
|
||||
$this->build_args = '';
|
||||
} else {
|
||||
|
|
@ -3819,7 +3858,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
$this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
|
||||
}
|
||||
|
||||
if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
|
||||
if ($this->dockerSecretsSupported && ! empty($this->build_secrets)) {
|
||||
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
|
||||
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
|
||||
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
|
||||
|
|
@ -3879,6 +3918,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||
return $composeFile;
|
||||
}
|
||||
|
||||
private function validatePathField(string $value, string $fieldName): string
|
||||
{
|
||||
if (! preg_match('/^\/[a-zA-Z0-9._\-\/]+$/', $value)) {
|
||||
throw new \RuntimeException("Invalid {$fieldName}: contains forbidden characters.");
|
||||
}
|
||||
if (str_contains($value, '..')) {
|
||||
throw new \RuntimeException("Invalid {$fieldName}: path traversal detected.");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function run_pre_deployment_command()
|
||||
{
|
||||
if (empty($this->application->pre_deployment_command)) {
|
||||
|
|
|
|||
|
|
@ -207,6 +207,9 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||
$serviceId = $labels->get('coolify.serviceId');
|
||||
$subType = $labels->get('coolify.service.subType');
|
||||
$subId = $labels->get('coolify.service.subId');
|
||||
if (empty($subId)) {
|
||||
continue;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$this->foundServiceApplicationIds->push($subId);
|
||||
// Store container status for aggregation
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ use Illuminate\Queue\Middleware\WithoutOverlapping;
|
|||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class ScheduledJobManager implements ShouldQueue
|
||||
{
|
||||
|
|
@ -27,6 +29,10 @@ class ScheduledJobManager implements ShouldQueue
|
|||
*/
|
||||
private ?Carbon $executionTime = null;
|
||||
|
||||
private int $dispatchedCount = 0;
|
||||
|
||||
private int $skippedCount = 0;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
|
|
@ -50,6 +56,11 @@ class ScheduledJobManager implements ShouldQueue
|
|||
*/
|
||||
public function middleware(): array
|
||||
{
|
||||
// Self-healing: clear any stale lock before WithoutOverlapping tries to acquire it.
|
||||
// Stale locks (TTL = -1) can occur during upgrades, Redis restarts, or edge cases.
|
||||
// @see https://github.com/coollabsio/coolify/issues/8327
|
||||
self::clearStaleLockIfPresent();
|
||||
|
||||
return [
|
||||
(new WithoutOverlapping('scheduled-job-manager'))
|
||||
->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks
|
||||
|
|
@ -57,10 +68,44 @@ class ScheduledJobManager implements ShouldQueue
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a stale WithoutOverlapping lock if it has no TTL (TTL = -1).
|
||||
*
|
||||
* This provides continuous self-healing since it runs every time the job is dispatched.
|
||||
* Stale locks permanently block all scheduled job executions with no user-visible error.
|
||||
*/
|
||||
private static function clearStaleLockIfPresent(): void
|
||||
{
|
||||
try {
|
||||
$cachePrefix = config('cache.prefix', '');
|
||||
$lockKey = $cachePrefix.'laravel-queue-overlap:'.self::class.':scheduled-job-manager';
|
||||
|
||||
$ttl = Redis::connection('default')->ttl($lockKey);
|
||||
|
||||
if ($ttl === -1) {
|
||||
Redis::connection('default')->del($lockKey);
|
||||
Log::channel('scheduled')->warning('Cleared stale ScheduledJobManager lock', [
|
||||
'lock_key' => $lockKey,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Never let lock cleanup failure prevent the job from running
|
||||
Log::channel('scheduled-errors')->error('Failed to check/clear stale lock', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
// Freeze the execution time at the start of the job
|
||||
$this->executionTime = Carbon::now();
|
||||
$this->dispatchedCount = 0;
|
||||
$this->skippedCount = 0;
|
||||
|
||||
Log::channel('scheduled')->info('ScheduledJobManager started', [
|
||||
'execution_time' => $this->executionTime->toIso8601String(),
|
||||
]);
|
||||
|
||||
// Process backups - don't let failures stop task processing
|
||||
try {
|
||||
|
|
@ -91,6 +136,20 @@ class ScheduledJobManager implements ShouldQueue
|
|||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
|
||||
Log::channel('scheduled')->info('ScheduledJobManager completed', [
|
||||
'execution_time' => $this->executionTime->toIso8601String(),
|
||||
'duration_ms' => $this->executionTime->diffInMilliseconds(Carbon::now()),
|
||||
'dispatched' => $this->dispatchedCount,
|
||||
'skipped' => $this->skippedCount,
|
||||
]);
|
||||
|
||||
// Write heartbeat so the UI can detect when the scheduler has stopped
|
||||
try {
|
||||
Cache::put('scheduled-job-manager:heartbeat', now()->toIso8601String(), 300);
|
||||
} catch (\Throwable) {
|
||||
// Non-critical; don't let heartbeat failure affect the job
|
||||
}
|
||||
}
|
||||
|
||||
private function processScheduledBackups(): void
|
||||
|
|
@ -101,8 +160,16 @@ class ScheduledJobManager implements ShouldQueue
|
|||
|
||||
foreach ($backups as $backup) {
|
||||
try {
|
||||
// Apply the same filtering logic as the original
|
||||
if (! $this->shouldProcessBackup($backup)) {
|
||||
$skipReason = $this->getBackupSkipReason($backup);
|
||||
if ($skipReason !== null) {
|
||||
$this->skippedCount++;
|
||||
$this->logSkip('backup', $skipReason, [
|
||||
'backup_id' => $backup->id,
|
||||
'database_id' => $backup->database_id,
|
||||
'database_type' => $backup->database_type,
|
||||
'team_id' => $backup->team_id ?? null,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +187,14 @@ class ScheduledJobManager implements ShouldQueue
|
|||
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||
DatabaseBackupJob::dispatch($backup);
|
||||
$this->dispatchedCount++;
|
||||
Log::channel('scheduled')->info('Backup dispatched', [
|
||||
'backup_id' => $backup->id,
|
||||
'database_id' => $backup->database_id,
|
||||
'database_type' => $backup->database_type,
|
||||
'team_id' => $backup->team_id ?? null,
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing backup', [
|
||||
|
|
@ -138,7 +213,15 @@ class ScheduledJobManager implements ShouldQueue
|
|||
|
||||
foreach ($tasks as $task) {
|
||||
try {
|
||||
if (! $this->shouldProcessTask($task)) {
|
||||
$skipReason = $this->getTaskSkipReason($task);
|
||||
if ($skipReason !== null) {
|
||||
$this->skippedCount++;
|
||||
$this->logSkip('task', $skipReason, [
|
||||
'task_id' => $task->id,
|
||||
'task_name' => $task->name,
|
||||
'team_id' => $task->server()?->team_id,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +239,13 @@ class ScheduledJobManager implements ShouldQueue
|
|||
|
||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||
ScheduledTaskJob::dispatch($task);
|
||||
$this->dispatchedCount++;
|
||||
Log::channel('scheduled')->info('Task dispatched', [
|
||||
'task_id' => $task->id,
|
||||
'task_name' => $task->name,
|
||||
'team_id' => $server->team_id,
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing task', [
|
||||
|
|
@ -166,33 +256,33 @@ class ScheduledJobManager implements ShouldQueue
|
|||
}
|
||||
}
|
||||
|
||||
private function shouldProcessBackup(ScheduledDatabaseBackup $backup): bool
|
||||
private function getBackupSkipReason(ScheduledDatabaseBackup $backup): ?string
|
||||
{
|
||||
if (blank(data_get($backup, 'database'))) {
|
||||
$backup->delete();
|
||||
|
||||
return false;
|
||||
return 'database_deleted';
|
||||
}
|
||||
|
||||
$server = $backup->server();
|
||||
if (blank($server)) {
|
||||
$backup->delete();
|
||||
|
||||
return false;
|
||||
return 'server_deleted';
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
return false;
|
||||
return 'server_not_functional';
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
return false;
|
||||
return 'subscription_unpaid';
|
||||
}
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
private function shouldProcessTask(ScheduledTask $task): bool
|
||||
private function getTaskSkipReason(ScheduledTask $task): ?string
|
||||
{
|
||||
$service = $task->service;
|
||||
$application = $task->application;
|
||||
|
|
@ -201,32 +291,32 @@ class ScheduledJobManager implements ShouldQueue
|
|||
if (blank($server)) {
|
||||
$task->delete();
|
||||
|
||||
return false;
|
||||
return 'server_deleted';
|
||||
}
|
||||
|
||||
if ($server->isFunctional() === false) {
|
||||
return false;
|
||||
return 'server_not_functional';
|
||||
}
|
||||
|
||||
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||
return false;
|
||||
return 'subscription_unpaid';
|
||||
}
|
||||
|
||||
if (! $service && ! $application) {
|
||||
$task->delete();
|
||||
|
||||
return false;
|
||||
return 'resource_deleted';
|
||||
}
|
||||
|
||||
if ($application && str($application->status)->contains('running') === false) {
|
||||
return false;
|
||||
return 'application_not_running';
|
||||
}
|
||||
|
||||
if ($service && str($service->status)->contains('running') === false) {
|
||||
return false;
|
||||
return 'service_not_running';
|
||||
}
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
private function shouldRunNow(string $frequency, string $timezone): bool
|
||||
|
|
@ -248,7 +338,15 @@ class ScheduledJobManager implements ShouldQueue
|
|||
|
||||
foreach ($servers as $server) {
|
||||
try {
|
||||
if (! $this->shouldProcessDockerCleanup($server)) {
|
||||
$skipReason = $this->getDockerCleanupSkipReason($server);
|
||||
if ($skipReason !== null) {
|
||||
$this->skippedCount++;
|
||||
$this->logSkip('docker_cleanup', $skipReason, [
|
||||
'server_id' => $server->id,
|
||||
'server_name' => $server->name,
|
||||
'team_id' => $server->team_id,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -270,6 +368,12 @@ class ScheduledJobManager implements ShouldQueue
|
|||
$server->settings->delete_unused_volumes,
|
||||
$server->settings->delete_unused_networks
|
||||
);
|
||||
$this->dispatchedCount++;
|
||||
Log::channel('scheduled')->info('Docker cleanup dispatched', [
|
||||
'server_id' => $server->id,
|
||||
'server_name' => $server->name,
|
||||
'team_id' => $server->team_id,
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('scheduled-errors')->error('Error processing docker cleanup', [
|
||||
|
|
@ -296,19 +400,28 @@ class ScheduledJobManager implements ShouldQueue
|
|||
return $query->get();
|
||||
}
|
||||
|
||||
private function shouldProcessDockerCleanup(Server $server): bool
|
||||
private function getDockerCleanupSkipReason(Server $server): ?string
|
||||
{
|
||||
if (! $server->isFunctional()) {
|
||||
return false;
|
||||
return 'server_not_functional';
|
||||
}
|
||||
|
||||
// In cloud, check subscription status (except team 0)
|
||||
if (isCloud() && $server->team_id !== 0) {
|
||||
if (data_get($server->team->subscription, 'stripe_invoice_paid', false) === false) {
|
||||
return false;
|
||||
return 'subscription_unpaid';
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
private function logSkip(string $type, string $reason, array $context = []): void
|
||||
{
|
||||
Log::channel('scheduled')->info(ucfirst(str_replace('_', ' ', $type)).' skipped', array_merge([
|
||||
'type' => $type,
|
||||
'skip_reason' => $reason,
|
||||
'execution_time' => $this->executionTime?->toIso8601String(),
|
||||
], $context));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
|||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -33,6 +34,19 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function failed(?\Throwable $exception): void
|
||||
{
|
||||
if ($exception instanceof \Illuminate\Queue\TimeoutExceededException) {
|
||||
Log::warning('ServerCheckJob timed out', [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
|
||||
// Delete the queue job so it doesn't appear in Horizon's failed list.
|
||||
$this->job?->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -101,12 +101,31 @@ class ServerConnectionCheckJob implements ShouldBeEncrypted, ShouldQueue
|
|||
'is_usable' => false,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(?\Throwable $exception): void
|
||||
{
|
||||
if ($exception instanceof \Illuminate\Queue\TimeoutExceededException) {
|
||||
Log::warning('ServerConnectionCheckJob timed out', [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
$this->server->settings->update([
|
||||
'is_reachable' => false,
|
||||
'is_usable' => false,
|
||||
]);
|
||||
|
||||
// Delete the queue job so it doesn't appear in Horizon's failed list.
|
||||
$this->job?->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function checkHetznerStatus(): void
|
||||
{
|
||||
$status = null;
|
||||
|
||||
try {
|
||||
$hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token);
|
||||
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Laravel\Horizon\Contracts\Silenced;
|
||||
|
||||
|
|
@ -28,6 +29,19 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue, Silenced
|
|||
|
||||
public function __construct(public Server $server, public int|string|null $percentage = null) {}
|
||||
|
||||
public function failed(?\Throwable $exception): void
|
||||
{
|
||||
if ($exception instanceof \Illuminate\Queue\TimeoutExceededException) {
|
||||
Log::warning('ServerStorageCheckJob timed out', [
|
||||
'server_id' => $this->server->id,
|
||||
'server_name' => $this->server->name,
|
||||
]);
|
||||
|
||||
// Delete the queue job so it doesn't appear in Horizon's failed list.
|
||||
$this->job?->delete();
|
||||
}
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1495,6 +1495,7 @@ class GlobalSearch extends Component
|
|||
'type' => 'one-click-service-'.$serviceKey,
|
||||
'category' => 'Services',
|
||||
'resourceType' => 'service',
|
||||
'logo' => data_get($service, 'logo'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class General extends Component
|
|||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerfile = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])]
|
||||
public ?string $dockerfileLocation = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
|
|
@ -85,7 +85,7 @@ class General extends Component
|
|||
#[Validate(['string', 'nullable'])]
|
||||
public ?string $dockerRegistryImageTag = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])]
|
||||
public ?string $dockerComposeLocation = null;
|
||||
|
||||
#[Validate(['string', 'nullable'])]
|
||||
|
|
|
|||
|
|
@ -163,10 +163,12 @@ class GithubPrivateRepository extends Component
|
|||
'selected_repository_owner' => $this->selected_repository_owner,
|
||||
'selected_repository_repo' => $this->selected_repository_repo,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
], [
|
||||
'selected_repository_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
|
||||
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||
'is_static' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
|
|
@ -75,6 +76,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||
'is_static' => 'required|boolean',
|
||||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class PublicGitRepository extends Component
|
|||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => 'nullable|string',
|
||||
'docker_compose_location' => 'nullable|string',
|
||||
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
|
|
@ -82,7 +82,7 @@ class PublicGitRepository extends Component
|
|||
'publish_directory' => 'nullable|string',
|
||||
'build_pack' => 'required|string',
|
||||
'base_directory' => 'nullable|string',
|
||||
'docker_compose_location' => 'nullable|string',
|
||||
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,25 @@ class HealthChecks extends Component
|
|||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = false;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['string', 'in:http,cmd'])]
|
||||
public string $healthCheckType = 'http';
|
||||
|
||||
#[Validate(['nullable', 'required_if:healthCheckType,cmd', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'])]
|
||||
public ?string $healthCheckCommand = null;
|
||||
|
||||
#[Validate(['required', 'string', 'in:GET,HEAD,POST,OPTIONS'])]
|
||||
public string $healthCheckMethod;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'in:http,https'])]
|
||||
public string $healthCheckScheme;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'])]
|
||||
public string $healthCheckHost;
|
||||
|
||||
#[Validate(['nullable', 'string'])]
|
||||
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
|
||||
public ?string $healthCheckPort = null;
|
||||
|
||||
#[Validate(['string'])]
|
||||
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
|
||||
public string $healthCheckPath;
|
||||
|
||||
#[Validate(['integer'])]
|
||||
|
|
@ -54,12 +60,14 @@ class HealthChecks extends Component
|
|||
|
||||
protected $rules = [
|
||||
'healthCheckEnabled' => 'boolean',
|
||||
'healthCheckPath' => 'string',
|
||||
'healthCheckPort' => 'nullable|string',
|
||||
'healthCheckHost' => 'string',
|
||||
'healthCheckMethod' => 'string',
|
||||
'healthCheckType' => 'string|in:http,cmd',
|
||||
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||
'healthCheckPort' => 'nullable|integer|min:1|max:65535',
|
||||
'healthCheckHost' => ['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||
'healthCheckMethod' => 'required|string|in:GET,HEAD,POST,OPTIONS',
|
||||
'healthCheckReturnCode' => 'integer',
|
||||
'healthCheckScheme' => 'string',
|
||||
'healthCheckScheme' => 'required|string|in:http,https',
|
||||
'healthCheckResponseText' => 'nullable|string',
|
||||
'healthCheckInterval' => 'integer|min:1',
|
||||
'healthCheckTimeout' => 'integer|min:1',
|
||||
|
|
@ -81,6 +89,8 @@ class HealthChecks extends Component
|
|||
|
||||
// Sync to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
|
@ -98,6 +108,8 @@ class HealthChecks extends Component
|
|||
} else {
|
||||
// Sync from model
|
||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
||||
$this->healthCheckType = $this->resource->health_check_type ?? 'http';
|
||||
$this->healthCheckCommand = $this->resource->health_check_command;
|
||||
$this->healthCheckMethod = $this->resource->health_check_method;
|
||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||
$this->healthCheckHost = $this->resource->health_check_host;
|
||||
|
|
@ -116,9 +128,12 @@ class HealthChecks extends Component
|
|||
public function instantSave()
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
$this->validate();
|
||||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
|
@ -143,6 +158,8 @@ class HealthChecks extends Component
|
|||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
|
@ -171,6 +188,8 @@ class HealthChecks extends Component
|
|||
|
||||
// Sync component properties to model
|
||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->resource->health_check_type = $this->healthCheckType;
|
||||
$this->resource->health_check_command = $this->healthCheckCommand;
|
||||
$this->resource->health_check_method = $this->healthCheckMethod;
|
||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||
$this->resource->health_check_host = $this->healthCheckHost;
|
||||
|
|
|
|||
|
|
@ -49,9 +49,10 @@ class ResourceOperations extends Component
|
|||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
$new_destination = StandaloneDocker::find($destination_id);
|
||||
$teamScope = fn ($q) => $q->where('team_id', currentTeam()->id);
|
||||
$new_destination = StandaloneDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
if (! $new_destination) {
|
||||
$new_destination = SwarmDocker::find($destination_id);
|
||||
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||
}
|
||||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
|
|
@ -352,7 +353,7 @@ class ResourceOperations extends Component
|
|||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
$new_environment = Environment::findOrFail($environment_id);
|
||||
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
|
||||
$this->resource->update([
|
||||
'environment_id' => $environment_id,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -60,10 +60,16 @@ class Show extends Component
|
|||
throw new \Exception('Certificate content cannot be empty.');
|
||||
}
|
||||
|
||||
if (! openssl_x509_read($this->certificateContent)) {
|
||||
$parsedCert = openssl_x509_read($this->certificateContent);
|
||||
if (! $parsedCert) {
|
||||
throw new \Exception('Invalid certificate format.');
|
||||
}
|
||||
|
||||
if (! openssl_x509_export($parsedCert, $cleanedCertificate)) {
|
||||
throw new \Exception('Failed to process certificate.');
|
||||
}
|
||||
$this->certificateContent = $cleanedCertificate;
|
||||
|
||||
if ($this->caCertificate) {
|
||||
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
||||
$this->caCertificate->save();
|
||||
|
|
@ -114,12 +120,14 @@ class Show extends Component
|
|||
{
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$base64Cert = base64_encode($this->certificateContent);
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$this->certificateContent}' > $caCertPath/coolify-ca.crt",
|
||||
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@
|
|||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\Server;
|
||||
use Cron\CronExpression;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -34,6 +39,53 @@ class DockerCleanup extends Component
|
|||
#[Validate('boolean')]
|
||||
public bool $disableApplicationImageRetention = false;
|
||||
|
||||
#[Computed]
|
||||
public function isCleanupStale(): bool
|
||||
{
|
||||
try {
|
||||
$lastExecution = DockerCleanupExecution::where('server_id', $this->server->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first();
|
||||
|
||||
if (! $lastExecution) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$frequency = $this->server->settings->docker_cleanup_frequency ?? '0 0 * * *';
|
||||
if (isset(VALID_CRON_STRINGS[$frequency])) {
|
||||
$frequency = VALID_CRON_STRINGS[$frequency];
|
||||
}
|
||||
|
||||
$cron = new CronExpression($frequency);
|
||||
$now = Carbon::now();
|
||||
$nextRun = Carbon::parse($cron->getNextRunDate($now));
|
||||
$afterThat = Carbon::parse($cron->getNextRunDate($nextRun));
|
||||
$intervalMinutes = $nextRun->diffInMinutes($afterThat);
|
||||
|
||||
$threshold = max($intervalMinutes * 2, 10);
|
||||
|
||||
return Carbon::parse($lastExecution->created_at)->diffInMinutes($now) > $threshold;
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function lastExecutionTime(): ?string
|
||||
{
|
||||
return DockerCleanupExecution::where('server_id', $this->server->id)
|
||||
->orderBy('created_at', 'desc')
|
||||
->first()
|
||||
?->created_at
|
||||
?->diffForHumans();
|
||||
}
|
||||
|
||||
#[Computed]
|
||||
public function isSchedulerHealthy(): bool
|
||||
{
|
||||
return Cache::get('scheduled-job-manager:heartbeat') !== null;
|
||||
}
|
||||
|
||||
public function mount(string $server_uuid)
|
||||
{
|
||||
try {
|
||||
|
|
|
|||
211
app/Livewire/Settings/ScheduledJobs.php
Normal file
211
app/Livewire/Settings/ScheduledJobs.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Models\DockerCleanupExecution;
|
||||
use App\Models\ScheduledDatabaseBackupExecution;
|
||||
use App\Models\ScheduledTaskExecution;
|
||||
use App\Services\SchedulerLogParser;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class ScheduledJobs extends Component
|
||||
{
|
||||
public string $filterType = 'all';
|
||||
|
||||
public string $filterDate = 'last_24h';
|
||||
|
||||
protected Collection $executions;
|
||||
|
||||
protected Collection $skipLogs;
|
||||
|
||||
protected Collection $managerRuns;
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->executions = collect();
|
||||
$this->skipLogs = collect();
|
||||
$this->managerRuns = collect();
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
if (! isInstanceAdmin()) {
|
||||
redirect()->route('dashboard');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function updatedFilterType(): void
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function updatedFilterDate(): void
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->loadData();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.scheduled-jobs', [
|
||||
'executions' => $this->executions,
|
||||
'skipLogs' => $this->skipLogs,
|
||||
'managerRuns' => $this->managerRuns,
|
||||
]);
|
||||
}
|
||||
|
||||
private function loadData(?int $teamId = null): void
|
||||
{
|
||||
$this->executions = $this->getExecutions($teamId);
|
||||
|
||||
$parser = new SchedulerLogParser;
|
||||
$this->skipLogs = $parser->getRecentSkips(50, $teamId);
|
||||
$this->managerRuns = $parser->getRecentRuns(30, $teamId);
|
||||
}
|
||||
|
||||
private function getExecutions(?int $teamId = null): Collection
|
||||
{
|
||||
$dateFrom = $this->getDateFrom();
|
||||
|
||||
$backups = collect();
|
||||
$tasks = collect();
|
||||
$cleanups = collect();
|
||||
|
||||
if ($this->filterType === 'all' || $this->filterType === 'backup') {
|
||||
$backups = $this->getBackupExecutions($dateFrom, $teamId);
|
||||
}
|
||||
|
||||
if ($this->filterType === 'all' || $this->filterType === 'task') {
|
||||
$tasks = $this->getTaskExecutions($dateFrom, $teamId);
|
||||
}
|
||||
|
||||
if ($this->filterType === 'all' || $this->filterType === 'cleanup') {
|
||||
$cleanups = $this->getCleanupExecutions($dateFrom, $teamId);
|
||||
}
|
||||
|
||||
return $backups->concat($tasks)->concat($cleanups)
|
||||
->sortByDesc('created_at')
|
||||
->values()
|
||||
->take(100);
|
||||
}
|
||||
|
||||
private function getBackupExecutions(?Carbon $dateFrom, ?int $teamId): Collection
|
||||
{
|
||||
$query = ScheduledDatabaseBackupExecution::with(['scheduledDatabaseBackup.database', 'scheduledDatabaseBackup.team'])
|
||||
->where('status', 'failed')
|
||||
->when($dateFrom, fn ($q) => $q->where('created_at', '>=', $dateFrom))
|
||||
->when($teamId, fn ($q) => $q->whereRelation('scheduledDatabaseBackup.team', 'id', $teamId))
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $query->map(function ($execution) {
|
||||
$backup = $execution->scheduledDatabaseBackup;
|
||||
$database = $backup?->database;
|
||||
$server = $backup?->server();
|
||||
|
||||
return [
|
||||
'id' => $execution->id,
|
||||
'type' => 'backup',
|
||||
'status' => $execution->status ?? 'unknown',
|
||||
'resource_name' => $database?->name ?? 'Deleted database',
|
||||
'resource_type' => $database ? class_basename($database) : null,
|
||||
'server_name' => $server?->name ?? 'Unknown',
|
||||
'server_id' => $server?->id,
|
||||
'team_id' => $backup?->team_id,
|
||||
'created_at' => $execution->created_at,
|
||||
'finished_at' => $execution->updated_at,
|
||||
'message' => $execution->message,
|
||||
'size' => $execution->size ?? null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getTaskExecutions(?Carbon $dateFrom, ?int $teamId): Collection
|
||||
{
|
||||
$query = ScheduledTaskExecution::with(['scheduledTask.application', 'scheduledTask.service'])
|
||||
->where('status', 'failed')
|
||||
->when($dateFrom, fn ($q) => $q->where('created_at', '>=', $dateFrom))
|
||||
->when($teamId, function ($q) use ($teamId) {
|
||||
$q->where(function ($sub) use ($teamId) {
|
||||
$sub->whereRelation('scheduledTask.application.environment.project.team', 'id', $teamId)
|
||||
->orWhereRelation('scheduledTask.service.environment.project.team', 'id', $teamId);
|
||||
});
|
||||
})
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $query->map(function ($execution) {
|
||||
$task = $execution->scheduledTask;
|
||||
$resource = $task?->application ?? $task?->service;
|
||||
$server = $task?->server();
|
||||
$teamId = $server?->team_id;
|
||||
|
||||
return [
|
||||
'id' => $execution->id,
|
||||
'type' => 'task',
|
||||
'status' => $execution->status ?? 'unknown',
|
||||
'resource_name' => $task?->name ?? 'Deleted task',
|
||||
'resource_type' => $resource ? class_basename($resource) : null,
|
||||
'server_name' => $server?->name ?? 'Unknown',
|
||||
'server_id' => $server?->id,
|
||||
'team_id' => $teamId,
|
||||
'created_at' => $execution->created_at,
|
||||
'finished_at' => $execution->finished_at,
|
||||
'message' => $execution->message,
|
||||
'size' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getCleanupExecutions(?Carbon $dateFrom, ?int $teamId): Collection
|
||||
{
|
||||
$query = DockerCleanupExecution::with(['server'])
|
||||
->where('status', 'failed')
|
||||
->when($dateFrom, fn ($q) => $q->where('created_at', '>=', $dateFrom))
|
||||
->when($teamId, fn ($q) => $q->whereRelation('server', 'team_id', $teamId))
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return $query->map(function ($execution) {
|
||||
$server = $execution->server;
|
||||
|
||||
return [
|
||||
'id' => $execution->id,
|
||||
'type' => 'cleanup',
|
||||
'status' => $execution->status ?? 'unknown',
|
||||
'resource_name' => $server?->name ?? 'Deleted server',
|
||||
'resource_type' => 'Server',
|
||||
'server_name' => $server?->name ?? 'Unknown',
|
||||
'server_id' => $server?->id,
|
||||
'team_id' => $server?->team_id,
|
||||
'created_at' => $execution->created_at,
|
||||
'finished_at' => $execution->finished_at ?? $execution->updated_at,
|
||||
'message' => $execution->message,
|
||||
'size' => null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function getDateFrom(): ?Carbon
|
||||
{
|
||||
return match ($this->filterDate) {
|
||||
'last_24h' => now()->subDay(),
|
||||
'last_7d' => now()->subWeek(),
|
||||
'last_30d' => now()->subMonth(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,8 @@ use Visus\Cuid2\Cuid2;
|
|||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
||||
'health_check_type' => ['type' => 'string', 'description' => 'Health check type: http or cmd.', 'enum' => ['http', 'cmd']],
|
||||
'health_check_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check command for CMD type.'],
|
||||
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
||||
|
|
@ -990,7 +992,7 @@ class Application extends BaseModel
|
|||
if (isDev() && data_get($this, 'private_key_id') === 0) {
|
||||
return 'deploy_key';
|
||||
}
|
||||
if (data_get($this, 'private_key_id')) {
|
||||
if (! is_null(data_get($this, 'private_key_id'))) {
|
||||
return 'deploy_key';
|
||||
} elseif (data_get($this, 'source')) {
|
||||
return 'source';
|
||||
|
|
@ -1959,16 +1961,6 @@ class Application extends BaseModel
|
|||
}
|
||||
}
|
||||
|
||||
public static function getDomainsByUuid(string $uuid): array
|
||||
{
|
||||
$application = self::where('uuid', $uuid)->first();
|
||||
|
||||
if ($application) {
|
||||
return $application->fqdns;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getLimits(): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,13 +5,35 @@ namespace App\Models;
|
|||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Scheduled Task model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the scheduled task in the database.'],
|
||||
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the scheduled task.'],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.'],
|
||||
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the scheduled task was created.'],
|
||||
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the scheduled task was last updated.'],
|
||||
],
|
||||
)]
|
||||
class ScheduledTask extends BaseModel
|
||||
{
|
||||
use HasSafeStringAttribute;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return static::where('team_id', $teamId)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,23 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Scheduled Task Execution model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the execution.'],
|
||||
'status' => ['type' => 'string', 'enum' => ['success', 'failed', 'running'], 'description' => 'The status of the execution.'],
|
||||
'message' => ['type' => 'string', 'nullable' => true, 'description' => 'The output message of the execution.'],
|
||||
'retry_count' => ['type' => 'integer', 'description' => 'The number of retries.'],
|
||||
'duration' => ['type' => 'number', 'nullable' => true, 'description' => 'Duration in seconds.'],
|
||||
'started_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'When the execution started.'],
|
||||
'finished_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'When the execution finished.'],
|
||||
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'When the record was created.'],
|
||||
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'When the record was last updated.'],
|
||||
],
|
||||
)]
|
||||
class ScheduledTaskExecution extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
|
|
|||
|
|
@ -1452,12 +1452,14 @@ $schema://$host {
|
|||
$certificateContent = $caCertificate->ssl_certificate;
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$base64Cert = base64_encode($certificateContent);
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$certificateContent}' > $caCertPath/coolify-ca.crt",
|
||||
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class ServiceApplication extends BaseModel
|
|||
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
return data_get($this, 'service.environment.project.team');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class ServiceDatabase extends BaseModel
|
|||
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
return data_get($this, 'service.environment.project.team');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
|
|
|
|||
|
|
@ -191,7 +191,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
|||
$this->getNotificationSettings('discord')?->isEnabled() ||
|
||||
$this->getNotificationSettings('slack')?->isEnabled() ||
|
||||
$this->getNotificationSettings('telegram')?->isEnabled() ||
|
||||
$this->getNotificationSettings('pushover')?->isEnabled();
|
||||
$this->getNotificationSettings('pushover')?->isEnabled() ||
|
||||
$this->getNotificationSettings('webhook')?->isEnabled();
|
||||
}
|
||||
|
||||
public function subscriptionEnded()
|
||||
|
|
|
|||
|
|
@ -37,8 +37,7 @@ class StandaloneDockerPolicy
|
|||
*/
|
||||
public function update(User $user, StandaloneDocker $standaloneDocker): bool
|
||||
{
|
||||
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||
return true;
|
||||
return $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,8 +45,7 @@ class StandaloneDockerPolicy
|
|||
*/
|
||||
public function delete(User $user, StandaloneDocker $standaloneDocker): bool
|
||||
{
|
||||
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||
return true;
|
||||
return $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,8 +53,7 @@ class StandaloneDockerPolicy
|
|||
*/
|
||||
public function restore(User $user, StandaloneDocker $standaloneDocker): bool
|
||||
{
|
||||
// return false;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,7 +61,6 @@ class StandaloneDockerPolicy
|
|||
*/
|
||||
public function forceDelete(User $user, StandaloneDocker $standaloneDocker): bool
|
||||
{
|
||||
// return false;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,8 +37,7 @@ class SwarmDockerPolicy
|
|||
*/
|
||||
public function update(User $user, SwarmDocker $swarmDocker): bool
|
||||
{
|
||||
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||
return true;
|
||||
return $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,8 +45,7 @@ class SwarmDockerPolicy
|
|||
*/
|
||||
public function delete(User $user, SwarmDocker $swarmDocker): bool
|
||||
{
|
||||
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||
return true;
|
||||
return $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,8 +53,7 @@ class SwarmDockerPolicy
|
|||
*/
|
||||
public function restore(User $user, SwarmDocker $swarmDocker): bool
|
||||
{
|
||||
// return false;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -64,7 +61,6 @@ class SwarmDockerPolicy
|
|||
*/
|
||||
public function forceDelete(User $user, SwarmDocker $swarmDocker): bool
|
||||
{
|
||||
// return false;
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
188
app/Services/SchedulerLogParser.php
Normal file
188
app/Services/SchedulerLogParser.php
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class SchedulerLogParser
|
||||
{
|
||||
/**
|
||||
* Get recent skip events from the scheduled log files.
|
||||
*
|
||||
* @return Collection<int, array{timestamp: string, type: string, reason: string, team_id: ?int, context: array}>
|
||||
*/
|
||||
public function getRecentSkips(int $limit = 100, ?int $teamId = null): Collection
|
||||
{
|
||||
$logFiles = $this->getLogFiles();
|
||||
|
||||
$skips = collect();
|
||||
|
||||
foreach ($logFiles as $logFile) {
|
||||
$lines = $this->readLastLines($logFile, 2000);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$entry = $this->parseLogLine($line);
|
||||
if ($entry === null || ! isset($entry['context']['skip_reason'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($teamId !== null && ($entry['context']['team_id'] ?? null) !== $teamId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$skips->push([
|
||||
'timestamp' => $entry['timestamp'],
|
||||
'type' => $entry['context']['type'] ?? 'unknown',
|
||||
'reason' => $entry['context']['skip_reason'],
|
||||
'team_id' => $entry['context']['team_id'] ?? null,
|
||||
'context' => $entry['context'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $skips->sortByDesc('timestamp')->values()->take($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent manager execution logs (start/complete events).
|
||||
*
|
||||
* @return Collection<int, array{timestamp: string, message: string, duration_ms: ?int, dispatched: ?int, skipped: ?int}>
|
||||
*/
|
||||
public function getRecentRuns(int $limit = 60, ?int $teamId = null): Collection
|
||||
{
|
||||
$logFiles = $this->getLogFiles();
|
||||
|
||||
$runs = collect();
|
||||
|
||||
foreach ($logFiles as $logFile) {
|
||||
$lines = $this->readLastLines($logFile, 2000);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$entry = $this->parseLogLine($line);
|
||||
if ($entry === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! str_contains($entry['message'], 'ScheduledJobManager')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$runs->push([
|
||||
'timestamp' => $entry['timestamp'],
|
||||
'message' => $entry['message'],
|
||||
'duration_ms' => $entry['context']['duration_ms'] ?? null,
|
||||
'dispatched' => $entry['context']['dispatched'] ?? null,
|
||||
'skipped' => $entry['context']['skipped'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $runs->sortByDesc('timestamp')->values()->take($limit);
|
||||
}
|
||||
|
||||
private function getLogFiles(): array
|
||||
{
|
||||
$logDir = storage_path('logs');
|
||||
if (! File::isDirectory($logDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$files = File::glob($logDir.'/scheduled-*.log');
|
||||
|
||||
// Sort by modification time, newest first
|
||||
usort($files, fn ($a, $b) => filemtime($b) - filemtime($a));
|
||||
|
||||
// Only check last 3 days of logs
|
||||
return array_slice($files, 0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{timestamp: string, level: string, message: string, context: array}|null
|
||||
*/
|
||||
private function parseLogLine(string $line): ?array
|
||||
{
|
||||
// Laravel daily log format: [2024-01-15 10:30:00] production.INFO: Message {"key":"value"}
|
||||
if (! preg_match('/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \w+\.(\w+): (.+)$/', $line, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = $matches[1];
|
||||
$level = $matches[2];
|
||||
$rest = $matches[3];
|
||||
|
||||
// Extract JSON context if present
|
||||
$context = [];
|
||||
if (preg_match('/^(.+?)\s+(\{.+\})\s*$/', $rest, $contextMatches)) {
|
||||
$message = $contextMatches[1];
|
||||
$decoded = json_decode($contextMatches[2], true);
|
||||
if (is_array($decoded)) {
|
||||
$context = $decoded;
|
||||
}
|
||||
} else {
|
||||
$message = $rest;
|
||||
}
|
||||
|
||||
return [
|
||||
'timestamp' => $timestamp,
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently read the last N lines of a file.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function readLastLines(string $filePath, int $lines): array
|
||||
{
|
||||
if (! File::exists($filePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fileSize = File::size($filePath);
|
||||
if ($fileSize === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For small files, read the whole thing
|
||||
if ($fileSize < 1024 * 1024) {
|
||||
$content = File::get($filePath);
|
||||
|
||||
return array_filter(explode("\n", $content), fn ($line) => $line !== '');
|
||||
}
|
||||
|
||||
// For large files, read from the end
|
||||
$handle = fopen($filePath, 'r');
|
||||
if ($handle === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$chunkSize = 8192;
|
||||
$buffer = '';
|
||||
$position = $fileSize;
|
||||
|
||||
while ($position > 0 && count($result) < $lines) {
|
||||
$readSize = min($chunkSize, $position);
|
||||
$position -= $readSize;
|
||||
fseek($handle, $position);
|
||||
$buffer = fread($handle, $readSize).$buffer;
|
||||
|
||||
$bufferLines = explode("\n", $buffer);
|
||||
$buffer = array_shift($bufferLines);
|
||||
|
||||
$result = array_merge(array_filter($bufferLines, fn ($line) => $line !== ''), $result);
|
||||
}
|
||||
|
||||
if ($buffer !== '' && count($result) < $lines) {
|
||||
array_unshift($result, $buffer);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
return array_slice($result, -$lines);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,9 +95,6 @@ trait SshRetryable
|
|||
if ($this->isRetryableSshError($lastErrorMessage) && $attempt < $maxRetries - 1) {
|
||||
$delay = $this->calculateRetryDelay($attempt);
|
||||
|
||||
// Track SSH retry event in Sentry
|
||||
$this->trackSshRetryEvent($attempt + 1, $maxRetries, $delay, $lastErrorMessage, $context);
|
||||
|
||||
// Add deployment log if available (for ExecuteRemoteCommand trait)
|
||||
if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) {
|
||||
$this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage);
|
||||
|
|
@ -133,42 +130,4 @@ trait SshRetryable
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track SSH retry event in Sentry
|
||||
*/
|
||||
protected function trackSshRetryEvent(int $attempt, int $maxRetries, int $delay, string $errorMessage, array $context = []): void
|
||||
{
|
||||
// Only track in production/cloud instances
|
||||
if (isDev() || ! config('constants.sentry.sentry_dsn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app('sentry')->captureMessage(
|
||||
'SSH connection retry triggered',
|
||||
\Sentry\Severity::warning(),
|
||||
[
|
||||
'extra' => [
|
||||
'attempt' => $attempt,
|
||||
'max_retries' => $maxRetries,
|
||||
'delay_seconds' => $delay,
|
||||
'error_message' => $errorMessage,
|
||||
'context' => $context,
|
||||
'retryable_error' => true,
|
||||
],
|
||||
'tags' => [
|
||||
'component' => 'ssh_retry',
|
||||
'error_type' => 'connection_retry',
|
||||
],
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Don't let Sentry tracking errors break the SSH retry flow
|
||||
Log::warning('Failed to track SSH retry event in Sentry', [
|
||||
'error' => $e->getMessage(),
|
||||
'original_attempt' => $attempt,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,12 +104,14 @@ function sharedDataApplications()
|
|||
'base_directory' => 'string|nullable',
|
||||
'publish_directory' => 'string|nullable',
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_path' => 'string',
|
||||
'health_check_port' => 'string|nullable',
|
||||
'health_check_host' => 'string',
|
||||
'health_check_method' => 'string',
|
||||
'health_check_type' => 'string|in:http,cmd',
|
||||
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||
'health_check_port' => 'integer|nullable|min:1|max:65535',
|
||||
'health_check_host' => ['string', 'regex:/^[a-zA-Z0-9.\-_]+$/'],
|
||||
'health_check_method' => 'string|in:GET,HEAD,POST,OPTIONS',
|
||||
'health_check_return_code' => 'numeric',
|
||||
'health_check_scheme' => 'string',
|
||||
'health_check_scheme' => 'string|in:http,https',
|
||||
'health_check_response_text' => 'string|nullable',
|
||||
'health_check_interval' => 'numeric',
|
||||
'health_check_timeout' => 'numeric',
|
||||
|
|
@ -132,8 +134,8 @@ function sharedDataApplications()
|
|||
'manual_webhook_secret_gitlab' => 'string|nullable',
|
||||
'manual_webhook_secret_bitbucket' => 'string|nullable',
|
||||
'manual_webhook_secret_gitea' => 'string|nullable',
|
||||
'dockerfile_location' => 'string|nullable',
|
||||
'docker_compose_location' => 'string',
|
||||
'dockerfile_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||
'docker_compose' => 'string|nullable',
|
||||
'docker_compose_domains' => 'array|nullable',
|
||||
'docker_compose_custom_start_command' => 'string|nullable',
|
||||
|
|
|
|||
|
|
@ -191,6 +191,10 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
$uuid = $overrides['uuid'] ?? (string) new Cuid2;
|
||||
$server = $destination->server;
|
||||
|
||||
if ($server->team_id !== currentTeam()->id) {
|
||||
throw new \RuntimeException('Destination does not belong to the current team.');
|
||||
}
|
||||
|
||||
// Prepare name and URL
|
||||
$name = $overrides['name'] ?? 'clone-of-'.str($source->name)->limit(20).'-'.$uuid;
|
||||
$applicationSettings = $source->settings;
|
||||
|
|
|
|||
|
|
@ -139,8 +139,9 @@ function checkMinimumDockerEngineVersion($dockerVersion)
|
|||
}
|
||||
function executeInDocker(string $containerId, string $command)
|
||||
{
|
||||
return "docker exec {$containerId} bash -c '{$command}'";
|
||||
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
|
||||
$escapedCommand = str_replace("'", "'\\''", $command);
|
||||
|
||||
return "docker exec {$containerId} bash -c '{$escapedCommand}'";
|
||||
}
|
||||
|
||||
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
|
||||
|
|
|
|||
|
|
@ -1233,7 +1233,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: true,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
|
|
@ -1246,7 +1246,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
network: $network,
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: true,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
|
|
@ -1260,7 +1260,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: true,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
|
|
@ -1271,7 +1271,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
network: $network,
|
||||
uuid: $uuid,
|
||||
domains: $fqdns,
|
||||
is_force_https_enabled: true,
|
||||
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||
serviceLabels: $serviceLabels,
|
||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ function currentTeam()
|
|||
|
||||
function showBoarding(): bool
|
||||
{
|
||||
if (isDev()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Auth::user()?->isMember()) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ function allowedPathsForUnsubscribedAccounts()
|
|||
'login',
|
||||
'logout',
|
||||
'force-password-reset',
|
||||
'two-factor-challenge',
|
||||
'livewire/update',
|
||||
'admin',
|
||||
];
|
||||
|
|
@ -95,6 +96,7 @@ function allowedPathsForInvalidAccounts()
|
|||
'logout',
|
||||
'verify',
|
||||
'force-password-reset',
|
||||
'two-factor-challenge',
|
||||
'livewire/update',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
"mockery/mockery": "^1.6.12",
|
||||
"nunomaduro/collision": "^8.8.3",
|
||||
"pestphp/pest": "^4.3.2",
|
||||
"pestphp/pest-plugin-browser": "^4.2",
|
||||
"phpstan/phpstan": "^2.1.38",
|
||||
"rector/rector": "^2.3.5",
|
||||
"serversideup/spin": "^3.1.1",
|
||||
|
|
|
|||
1565
composer.lock
generated
1565
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.463',
|
||||
'version' => '4.0.0-beta.464',
|
||||
'helper_version' => '1.0.12',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
|
|
|||
|
|
@ -54,18 +54,10 @@ return [
|
|||
],
|
||||
|
||||
'testing' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DATABASE_TEST_URL'),
|
||||
'host' => env('DB_TEST_HOST', 'postgres'),
|
||||
'port' => env('DB_TEST_PORT', '5432'),
|
||||
'database' => env('DB_TEST_DATABASE', 'coolify_test'),
|
||||
'username' => env('DB_TEST_USERNAME', 'coolify'),
|
||||
'password' => env('DB_TEST_PASSWORD', 'password'),
|
||||
'charset' => 'utf8',
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
'foreign_key_constraints' => true,
|
||||
],
|
||||
|
||||
],
|
||||
|
|
|
|||
|
|
@ -184,13 +184,13 @@ return [
|
|||
'connection' => 'redis',
|
||||
'balance' => env('HORIZON_BALANCE', 'false'),
|
||||
'queue' => env('HORIZON_QUEUES', 'high,default'),
|
||||
'maxTime' => 3600,
|
||||
'maxTime' => env('HORIZON_MAX_TIME', 0),
|
||||
'maxJobs' => 400,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'nice' => 0,
|
||||
'sleep' => 3,
|
||||
'timeout' => 3600,
|
||||
'timeout' => env('HORIZON_TIMEOUT', 36000),
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
|||
21
database/factories/ScheduledTaskFactory.php
Normal file
21
database/factories/ScheduledTaskFactory.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class ScheduledTaskFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->word(),
|
||||
'command' => 'echo hello',
|
||||
'frequency' => '* * * * *',
|
||||
'timeout' => 300,
|
||||
'enabled' => true,
|
||||
'team_id' => Team::factory(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,10 @@ class AddIndexToActivityLog extends Migration
|
|||
{
|
||||
public function up()
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb');
|
||||
DB::statement('CREATE INDEX idx_activity_type_uuid ON activity_log USING GIN (properties jsonb_path_ops)');
|
||||
|
|
@ -18,6 +22,10 @@ class AddIndexToActivityLog extends Migration
|
|||
|
||||
public function down()
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ return new class extends Migration
|
|||
|
||||
public function up(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
||||
if (! $this->indexExists($indexName)) {
|
||||
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
||||
|
|
@ -32,6 +36,10 @@ return new class extends Migration
|
|||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->indexes as [, , $indexName]) {
|
||||
DB::statement("DROP INDEX IF EXISTS \"{$indexName}\"");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<?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
|
||||
{
|
||||
if (! Schema::hasColumn('applications', 'health_check_type')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->text('health_check_type')->default('http')->after('health_check_enabled');
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('applications', 'health_check_command')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->text('health_check_command')->nullable()->after('health_check_type');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('applications', 'health_check_type')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('health_check_type');
|
||||
});
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('applications', 'health_check_command')) {
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('health_check_command');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
1754
database/schema/testing-schema.sql
Normal file
1754
database/schema/testing-schema.sql
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,7 @@ class ApplicationSeeder extends Seeder
|
|||
'git_repository' => 'coollabsio/coolify-examples',
|
||||
'git_branch' => 'v4.x',
|
||||
'base_directory' => '/docker-compose',
|
||||
'docker_compose_location' => 'docker-compose-test.yaml',
|
||||
'docker_compose_location' => '/docker-compose-test.yaml',
|
||||
'build_pack' => 'dockercompose',
|
||||
'ports_exposes' => '80',
|
||||
'environment_id' => 1,
|
||||
|
|
|
|||
|
|
@ -26,12 +26,14 @@ class CaSslCertSeeder extends Seeder
|
|||
}
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$base64Cert = base64_encode($caCert->ssl_certificate);
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$caCert->ssl_certificate}' > $caCertPath/coolify-ca.crt",
|
||||
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
|
|
|
|||
210
docker-compose-maxio.dev.yml
Normal file
210
docker-compose-maxio.dev.yml
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
services:
|
||||
coolify:
|
||||
image: coolify:dev
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/development/Dockerfile
|
||||
args:
|
||||
- USER_ID=${USERID:-1000}
|
||||
- GROUP_ID=${GROUPID:-1000}
|
||||
ports:
|
||||
- "${APP_PORT:-8000}:8080"
|
||||
environment:
|
||||
AUTORUN_ENABLED: false
|
||||
PUSHER_HOST: "${PUSHER_HOST}"
|
||||
PUSHER_PORT: "${PUSHER_PORT}"
|
||||
PUSHER_SCHEME: "${PUSHER_SCHEME:-http}"
|
||||
PUSHER_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||
PUSHER_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
PUSHER_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||
healthcheck:
|
||||
test: curl -sf http://127.0.0.1:8080/api/health || exit 1
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
volumes:
|
||||
- .:/var/www/html/:cached
|
||||
- dev_backups_data:/var/www/html/storage/app/backups
|
||||
networks:
|
||||
- coolify
|
||||
postgres:
|
||||
pull_policy: always
|
||||
ports:
|
||||
- "${FORWARD_DB_PORT:-5432}:5432"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
POSTGRES_USER: "${DB_USERNAME:-coolify}"
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD:-password}"
|
||||
POSTGRES_DB: "${DB_DATABASE:-coolify}"
|
||||
POSTGRES_HOST_AUTH_METHOD: "trust"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
volumes:
|
||||
- dev_postgres_data:/var/lib/postgresql/data
|
||||
redis:
|
||||
pull_policy: always
|
||||
ports:
|
||||
- "${FORWARD_REDIS_PORT:-6379}:6379"
|
||||
env_file:
|
||||
- .env
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
volumes:
|
||||
- dev_redis_data:/data
|
||||
soketi:
|
||||
image: coolify-realtime:dev
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/coolify-realtime/Dockerfile
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "${FORWARD_SOKETI_PORT:-6001}:6001"
|
||||
- "6002:6002"
|
||||
volumes:
|
||||
- ./storage:/var/www/html/storage
|
||||
- ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js
|
||||
environment:
|
||||
SOKETI_DEBUG: "false"
|
||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:6001/ready && curl -fsS http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
retries: 10
|
||||
timeout: 2s
|
||||
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
|
||||
vite:
|
||||
image: node:24-alpine
|
||||
pull_policy: always
|
||||
container_name: coolify-vite
|
||||
working_dir: /var/www/html
|
||||
environment:
|
||||
VITE_HOST: "${VITE_HOST:-localhost}"
|
||||
VITE_PORT: "${VITE_PORT:-5173}"
|
||||
ports:
|
||||
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
|
||||
volumes:
|
||||
- .:/var/www/html/:cached
|
||||
command: sh -c "npm install && npm run dev"
|
||||
networks:
|
||||
- coolify
|
||||
testing-host:
|
||||
image: coolify-testing-host:dev
|
||||
pull_policy: never
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/testing-host/Dockerfile
|
||||
init: true
|
||||
container_name: coolify-testing-host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- dev_coolify_data:/data/coolify
|
||||
- dev_backups_data:/data/coolify/backups
|
||||
- dev_postgres_data:/data/coolify/_volumes/database
|
||||
- dev_redis_data:/data/coolify/_volumes/redis
|
||||
- dev_minio_data:/data/coolify/_volumes/minio
|
||||
networks:
|
||||
- coolify
|
||||
mailpit:
|
||||
image: axllent/mailpit:latest
|
||||
pull_policy: always
|
||||
container_name: coolify-mail
|
||||
ports:
|
||||
- "${FORWARD_MAILPIT_PORT:-1025}:1025"
|
||||
- "${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025"
|
||||
networks:
|
||||
- coolify
|
||||
# maxio:
|
||||
# image: ghcr.io/coollabsio/maxio
|
||||
# pull_policy: always
|
||||
# container_name: coolify-maxio
|
||||
# ports:
|
||||
# - "${FORWARD_MAXIO_PORT:-9000}:9000"
|
||||
# environment:
|
||||
# MAXIO_ACCESS_KEY: "${MAXIO_ACCESS_KEY:-maxioadmin}"
|
||||
# MAXIO_SECRET_KEY: "${MAXIO_SECRET_KEY:-maxioadmin}"
|
||||
# volumes:
|
||||
# - dev_maxio_data:/data
|
||||
# networks:
|
||||
# - coolify
|
||||
minio:
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
pull_policy: always
|
||||
container_name: coolify-minio
|
||||
command: server /data --console-address ":9001"
|
||||
ports:
|
||||
- "${FORWARD_MINIO_PORT:-9000}:9000"
|
||||
- "${FORWARD_MINIO_PORT_CONSOLE:-9001}:9001"
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
|
||||
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
|
||||
volumes:
|
||||
- dev_minio_data:/data
|
||||
- dev_maxio_data:/data
|
||||
networks:
|
||||
- coolify
|
||||
# maxio-init:
|
||||
# image: minio/mc:latest
|
||||
# pull_policy: always
|
||||
# container_name: coolify-maxio-init
|
||||
# restart: no
|
||||
# depends_on:
|
||||
# - maxio
|
||||
# entrypoint: >
|
||||
# /bin/sh -c "
|
||||
# echo 'Waiting for MaxIO to be ready...';
|
||||
# until mc alias set local http://coolify-maxio:9000 maxioadmin maxioadmin 2>/dev/null; do
|
||||
# echo 'MaxIO not ready yet, waiting...';
|
||||
# sleep 2;
|
||||
# done;
|
||||
# echo 'MaxIO is ready, creating bucket if needed...';
|
||||
# mc mb local/local --ignore-existing;
|
||||
# echo 'MaxIO initialization complete - bucket local is ready';
|
||||
# "
|
||||
# networks:
|
||||
# - coolify
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
pull_policy: always
|
||||
container_name: coolify-minio-init
|
||||
restart: no
|
||||
depends_on:
|
||||
- minio
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
echo 'Waiting for MinIO to be ready...';
|
||||
until mc alias set local http://coolify-minio:9000 minioadmin minioadmin 2>/dev/null; do
|
||||
echo 'MinIO not ready yet, waiting...';
|
||||
sleep 2;
|
||||
done;
|
||||
echo 'MinIO is ready, creating bucket if needed...';
|
||||
mc mb local/local --ignore-existing;
|
||||
echo 'MinIO initialization complete - bucket local is ready';
|
||||
"
|
||||
networks:
|
||||
- coolify
|
||||
|
||||
volumes:
|
||||
dev_backups_data:
|
||||
dev_postgres_data:
|
||||
dev_redis_data:
|
||||
dev_coolify_data:
|
||||
dev_minio_data:
|
||||
dev_maxio_data:
|
||||
|
||||
networks:
|
||||
coolify:
|
||||
name: coolify
|
||||
external: false
|
||||
|
|
@ -26,6 +26,8 @@ services:
|
|||
volumes:
|
||||
- .:/var/www/html/:cached
|
||||
- dev_backups_data:/var/www/html/storage/app/backups
|
||||
networks:
|
||||
- coolify
|
||||
postgres:
|
||||
pull_policy: always
|
||||
ports:
|
||||
|
|
@ -76,6 +78,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:6001/ready && curl -fsS http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ RUN apk add --no-cache \
|
|||
lsof \
|
||||
vim
|
||||
|
||||
# Install PHP extensions
|
||||
RUN install-php-extensions sockets
|
||||
|
||||
# Configure shell aliases
|
||||
RUN echo "alias ll='ls -al'" >> /etc/profile && \
|
||||
echo "alias a='php artisan'" >> /etc/profile && \
|
||||
|
|
|
|||
815
openapi.json
815
openapi.json
|
|
@ -8049,6 +8049,698 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/scheduled-tasks": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "List Tasks",
|
||||
"description": "List all scheduled tasks for an application.",
|
||||
"operationId": "list-scheduled-tasks-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get all scheduled tasks for an application.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Create Task",
|
||||
"description": "Create a new scheduled task for an application.",
|
||||
"operationId": "create-scheduled-task-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Scheduled task data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"name",
|
||||
"command",
|
||||
"frequency"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scheduled task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "The frequency of the scheduled task."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The container where the command should be executed."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The timeout of the scheduled task in seconds.",
|
||||
"default": 300
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the scheduled task is enabled.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Scheduled task created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/scheduled-tasks\/{task_uuid}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Delete Task",
|
||||
"description": "Delete a scheduled task for an application.",
|
||||
"operationId": "delete-scheduled-task-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Scheduled task deleted.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Scheduled task deleted."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Update Task",
|
||||
"description": "Update a scheduled task for an application.",
|
||||
"operationId": "update-scheduled-task-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Scheduled task data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scheduled task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "The frequency of the scheduled task."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The container where the command should be executed."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The timeout of the scheduled task in seconds.",
|
||||
"default": 300
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the scheduled task is enabled.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Scheduled task updated.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/applications\/{uuid}\/scheduled-tasks\/{task_uuid}\/executions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "List Executions",
|
||||
"description": "List all executions for a scheduled task on an application.",
|
||||
"operationId": "list-scheduled-task-executions-by-application-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the application.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get all executions for a scheduled task.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTaskExecution"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/services\/{uuid}\/scheduled-tasks": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "List Tasks",
|
||||
"description": "List all scheduled tasks for a service.",
|
||||
"operationId": "list-scheduled-tasks-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get all scheduled tasks for a service.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Create Task",
|
||||
"description": "Create a new scheduled task for a service.",
|
||||
"operationId": "create-scheduled-task-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Scheduled task data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"required": [
|
||||
"name",
|
||||
"command",
|
||||
"frequency"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scheduled task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "The frequency of the scheduled task."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The container where the command should be executed."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The timeout of the scheduled task in seconds.",
|
||||
"default": 300
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the scheduled task is enabled.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Scheduled task created.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/services\/{uuid}\/scheduled-tasks\/{task_uuid}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Delete Task",
|
||||
"description": "Delete a scheduled task for a service.",
|
||||
"operationId": "delete-scheduled-task-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Scheduled task deleted.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Scheduled task deleted."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "Update Task",
|
||||
"description": "Update a scheduled task for a service.",
|
||||
"operationId": "update-scheduled-task-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Scheduled task data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scheduled task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "The frequency of the scheduled task."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The container where the command should be executed."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The timeout of the scheduled task in seconds.",
|
||||
"default": 300
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the scheduled task is enabled.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Scheduled task updated.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTask"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
},
|
||||
"422": {
|
||||
"$ref": "#\/components\/responses\/422"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/services\/{uuid}\/scheduled-tasks\/{task_uuid}\/executions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Scheduled Tasks"
|
||||
],
|
||||
"summary": "List Executions",
|
||||
"description": "List all executions for a scheduled task on a service.",
|
||||
"operationId": "list-scheduled-task-executions-by-service-uuid",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the service.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "task_uuid",
|
||||
"in": "path",
|
||||
"description": "UUID of the scheduled task.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get all executions for a scheduled task.",
|
||||
"content": {
|
||||
"application\/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#\/components\/schemas\/ScheduledTaskExecution"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#\/components\/responses\/401"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#\/components\/responses\/404"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"\/security\/keys": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
|
@ -10332,6 +11024,19 @@
|
|||
"type": "integer",
|
||||
"description": "Health check start period in seconds."
|
||||
},
|
||||
"health_check_type": {
|
||||
"type": "string",
|
||||
"description": "Health check type: http or cmd.",
|
||||
"enum": [
|
||||
"http",
|
||||
"cmd"
|
||||
]
|
||||
},
|
||||
"health_check_command": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Health check command for CMD type."
|
||||
},
|
||||
"limits_memory": {
|
||||
"type": "string",
|
||||
"description": "Memory limit."
|
||||
|
|
@ -10771,6 +11476,110 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ScheduledTask": {
|
||||
"description": "Scheduled Task model",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"description": "The unique identifier of the scheduled task in the database."
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier of the scheduled task."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "The flag to indicate if the scheduled task is enabled."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the scheduled task."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute."
|
||||
},
|
||||
"frequency": {
|
||||
"type": "string",
|
||||
"description": "The frequency of the scheduled task."
|
||||
},
|
||||
"container": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The container where the command should be executed."
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "The timeout of the scheduled task in seconds."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the scheduled task was created."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date and time when the scheduled task was last updated."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ScheduledTaskExecution": {
|
||||
"description": "Scheduled Task Execution model",
|
||||
"properties": {
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier of the execution."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"success",
|
||||
"failed",
|
||||
"running"
|
||||
],
|
||||
"description": "The status of the execution."
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The output message of the execution."
|
||||
},
|
||||
"retry_count": {
|
||||
"type": "integer",
|
||||
"description": "The number of retries."
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"nullable": true,
|
||||
"description": "Duration in seconds."
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"description": "When the execution started."
|
||||
},
|
||||
"finished_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"description": "When the execution finished."
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the record was created."
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the record was last updated."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"Server": {
|
||||
"description": "Server model",
|
||||
"properties": {
|
||||
|
|
@ -11283,6 +12092,10 @@
|
|||
"name": "Resources",
|
||||
"description": "Resources"
|
||||
},
|
||||
{
|
||||
"name": "Scheduled Tasks",
|
||||
"description": "Scheduled Tasks"
|
||||
},
|
||||
{
|
||||
"name": "Private Keys",
|
||||
"description": "Private Keys"
|
||||
|
|
@ -11300,4 +12113,4 @@
|
|||
"description": "Teams"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
565
openapi.yaml
565
openapi.yaml
|
|
@ -5085,6 +5085,478 @@ paths:
|
|||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/scheduled-tasks':
|
||||
get:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'List Tasks'
|
||||
description: 'List all scheduled tasks for an application.'
|
||||
operationId: list-scheduled-tasks-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get all scheduled tasks for an application.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Create Task'
|
||||
description: 'Create a new scheduled task for an application.'
|
||||
operationId: create-scheduled-task-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Scheduled task data'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- name
|
||||
- command
|
||||
- frequency
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 'The name of the scheduled task.'
|
||||
command:
|
||||
type: string
|
||||
description: 'The command to execute.'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'The frequency of the scheduled task.'
|
||||
container:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The container where the command should be executed.'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'The timeout of the scheduled task in seconds.'
|
||||
default: 300
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the scheduled task is enabled.'
|
||||
default: true
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
description: 'Scheduled task created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/scheduled-tasks/{task_uuid}':
|
||||
delete:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Delete Task'
|
||||
description: 'Delete a scheduled task for an application.'
|
||||
operationId: delete-scheduled-task-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Scheduled task deleted.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Scheduled task deleted.' }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Update Task'
|
||||
description: 'Update a scheduled task for an application.'
|
||||
operationId: update-scheduled-task-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Scheduled task data'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 'The name of the scheduled task.'
|
||||
command:
|
||||
type: string
|
||||
description: 'The command to execute.'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'The frequency of the scheduled task.'
|
||||
container:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The container where the command should be executed.'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'The timeout of the scheduled task in seconds.'
|
||||
default: 300
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the scheduled task is enabled.'
|
||||
default: true
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: 'Scheduled task updated.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/applications/{uuid}/scheduled-tasks/{task_uuid}/executions':
|
||||
get:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'List Executions'
|
||||
description: 'List all executions for a scheduled task on an application.'
|
||||
operationId: list-scheduled-task-executions-by-application-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the application.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get all executions for a scheduled task.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ScheduledTaskExecution'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/services/{uuid}/scheduled-tasks':
|
||||
get:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'List Tasks'
|
||||
description: 'List all scheduled tasks for a service.'
|
||||
operationId: list-scheduled-tasks-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get all scheduled tasks for a service.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
post:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Create Task'
|
||||
description: 'Create a new scheduled task for a service.'
|
||||
operationId: create-scheduled-task-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Scheduled task data'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- name
|
||||
- command
|
||||
- frequency
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 'The name of the scheduled task.'
|
||||
command:
|
||||
type: string
|
||||
description: 'The command to execute.'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'The frequency of the scheduled task.'
|
||||
container:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The container where the command should be executed.'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'The timeout of the scheduled task in seconds.'
|
||||
default: 300
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the scheduled task is enabled.'
|
||||
default: true
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
description: 'Scheduled task created.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/services/{uuid}/scheduled-tasks/{task_uuid}':
|
||||
delete:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Delete Task'
|
||||
description: 'Delete a scheduled task for a service.'
|
||||
operationId: delete-scheduled-task-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Scheduled task deleted.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
message: { type: string, example: 'Scheduled task deleted.' }
|
||||
type: object
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
patch:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'Update Task'
|
||||
description: 'Update a scheduled task for a service.'
|
||||
operationId: update-scheduled-task-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: 'Scheduled task data'
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: 'The name of the scheduled task.'
|
||||
command:
|
||||
type: string
|
||||
description: 'The command to execute.'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'The frequency of the scheduled task.'
|
||||
container:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The container where the command should be executed.'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'The timeout of the scheduled task in seconds.'
|
||||
default: 300
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the scheduled task is enabled.'
|
||||
default: true
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: 'Scheduled task updated.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ScheduledTask'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
'422':
|
||||
$ref: '#/components/responses/422'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
'/services/{uuid}/scheduled-tasks/{task_uuid}/executions':
|
||||
get:
|
||||
tags:
|
||||
- 'Scheduled Tasks'
|
||||
summary: 'List Executions'
|
||||
description: 'List all executions for a scheduled task on a service.'
|
||||
operationId: list-scheduled-task-executions-by-service-uuid
|
||||
parameters:
|
||||
-
|
||||
name: uuid
|
||||
in: path
|
||||
description: 'UUID of the service.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: task_uuid
|
||||
in: path
|
||||
description: 'UUID of the scheduled task.'
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get all executions for a scheduled task.'
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ScheduledTaskExecution'
|
||||
'401':
|
||||
$ref: '#/components/responses/401'
|
||||
'404':
|
||||
$ref: '#/components/responses/404'
|
||||
security:
|
||||
-
|
||||
bearerAuth: []
|
||||
/security/keys:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -6488,6 +6960,16 @@ components:
|
|||
health_check_start_period:
|
||||
type: integer
|
||||
description: 'Health check start period in seconds.'
|
||||
health_check_type:
|
||||
type: string
|
||||
description: 'Health check type: http or cmd.'
|
||||
enum:
|
||||
- http
|
||||
- cmd
|
||||
health_check_command:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'Health check command for CMD type.'
|
||||
limits_memory:
|
||||
type: string
|
||||
description: 'Memory limit.'
|
||||
|
|
@ -6807,6 +7289,86 @@ components:
|
|||
description:
|
||||
type: string
|
||||
type: object
|
||||
ScheduledTask:
|
||||
description: 'Scheduled Task model'
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 'The unique identifier of the scheduled task in the database.'
|
||||
uuid:
|
||||
type: string
|
||||
description: 'The unique identifier of the scheduled task.'
|
||||
enabled:
|
||||
type: boolean
|
||||
description: 'The flag to indicate if the scheduled task is enabled.'
|
||||
name:
|
||||
type: string
|
||||
description: 'The name of the scheduled task.'
|
||||
command:
|
||||
type: string
|
||||
description: 'The command to execute.'
|
||||
frequency:
|
||||
type: string
|
||||
description: 'The frequency of the scheduled task.'
|
||||
container:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The container where the command should be executed.'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'The timeout of the scheduled task in seconds.'
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 'The date and time when the scheduled task was created.'
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 'The date and time when the scheduled task was last updated.'
|
||||
type: object
|
||||
ScheduledTaskExecution:
|
||||
description: 'Scheduled Task Execution model'
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
description: 'The unique identifier of the execution.'
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- success
|
||||
- failed
|
||||
- running
|
||||
description: 'The status of the execution.'
|
||||
message:
|
||||
type: string
|
||||
nullable: true
|
||||
description: 'The output message of the execution.'
|
||||
retry_count:
|
||||
type: integer
|
||||
description: 'The number of retries.'
|
||||
duration:
|
||||
type: number
|
||||
nullable: true
|
||||
description: 'Duration in seconds.'
|
||||
started_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: 'When the execution started.'
|
||||
finished_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: 'When the execution finished.'
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 'When the record was created.'
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 'When the record was last updated.'
|
||||
type: object
|
||||
Server:
|
||||
description: 'Server model'
|
||||
properties:
|
||||
|
|
@ -7161,6 +7723,9 @@ tags:
|
|||
-
|
||||
name: Resources
|
||||
description: Resources
|
||||
-
|
||||
name: 'Scheduled Tasks'
|
||||
description: 'Scheduled Tasks'
|
||||
-
|
||||
name: 'Private Keys'
|
||||
description: 'Private Keys'
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ services:
|
|||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
||||
interval: 5s
|
||||
|
|
|
|||
65
package-lock.json
generated
65
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
|||
"@tailwindcss/typography": "0.5.16",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ioredis": "5.6.1"
|
||||
"ioredis": "5.6.1",
|
||||
"playwright": "^1.58.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
|
|
@ -949,7 +950,8 @@
|
|||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.5.10",
|
||||
|
|
@ -1402,8 +1404,7 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
|
|
@ -1556,6 +1557,7 @@
|
|||
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
|
|
@ -1570,6 +1572,7 @@
|
|||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
|
|
@ -2330,7 +2333,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2338,6 +2340,50 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
|
|
@ -2407,7 +2453,6 @@
|
|||
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
|
|
@ -2512,6 +2557,7 @@
|
|||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
|
|
@ -2556,8 +2602,7 @@
|
|||
"version": "4.1.18",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
|
|
@ -2609,7 +2654,6 @@
|
|||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -2709,7 +2753,6 @@
|
|||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.26",
|
||||
"@vue/compiler-sfc": "3.5.26",
|
||||
|
|
@ -2732,6 +2775,7 @@
|
|||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
|
@ -2753,6 +2797,7 @@
|
|||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"@tailwindcss/typography": "0.5.16",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "5.5.0",
|
||||
"ioredis": "5.6.1"
|
||||
"ioredis": "5.6.1",
|
||||
"playwright": "^1.58.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
phpunit.xml
15
phpunit.xml
|
|
@ -7,17 +7,20 @@
|
|||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">./tests/Feature</directory>
|
||||
</testsuite>
|
||||
<testsuite name="v4">
|
||||
<directory suffix="Test.php">./tests/v4</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage/>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="DB_CONNECTION" value="testing"/>
|
||||
<env name="DB_TEST_DATABASE" value="coolify"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="CACHE_DRIVER" value="array" force="true"/>
|
||||
<env name="DB_CONNECTION" value="testing" force="true"/>
|
||||
|
||||
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
<source>
|
||||
|
|
|
|||
BIN
public/svgs/spacebot.png
Normal file
BIN
public/svgs/spacebot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||
if (! function_exists('getOldOrLocal')) {
|
||||
function getOldOrLocal($key, $localValue)
|
||||
{
|
||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||
}
|
||||
}
|
||||
|
||||
$name = getOldOrLocal('name', 'test3 normal user');
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
{{-- Eye-off icon (shown when password is visible) --}}
|
||||
<svg x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
<svg x-cloak x-show="type === 'text'" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@
|
|||
href="{{ route('settings.oauth') }}">
|
||||
OAuth
|
||||
</a>
|
||||
<a class="{{ request()->routeIs('settings.scheduled-jobs') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.scheduled-jobs') }}">
|
||||
Scheduled Jobs
|
||||
</a>
|
||||
<div class="flex-1"></div>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
<div>
|
||||
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
|
||||
<h1 class="mt-4 font-bold tracking-tight dark:text-white">This page is definitely old, not like you!</h1>
|
||||
<p class="text-base leading-7 dark:text-neutral-300 text-black">Sorry, we couldn't find the page you're looking
|
||||
for.
|
||||
<p class="text-base leading-7 dark:text-neutral-300 text-black">Your session has expired. Please log in again to continue.
|
||||
</p>
|
||||
<div class="flex items-center mt-10 gap-x-2">
|
||||
<a href="{{ url()->previous() }}">
|
||||
<x-forms.button>Go back</x-forms.button>
|
||||
</a>
|
||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
||||
<x-forms.button>Dashboard</x-forms.button>
|
||||
<a href="/login">
|
||||
<x-forms.button>Back to Login</x-forms.button>
|
||||
</a>
|
||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||
support
|
||||
|
|
|
|||
|
|
@ -699,15 +699,21 @@
|
|||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
@if (! empty($item['logo']))
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="font-medium text-neutral-900 dark:text-white truncate">
|
||||
|
|
@ -808,15 +814,22 @@
|
|||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<template x-if="item.logo">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!item.logo">
|
||||
<div
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 text-warning-600 dark:text-warning-400"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="font-medium text-neutral-900 dark:text-white truncate"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||
<div class="navbar-main">
|
||||
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.application.configuration', $parameters) }}">
|
||||
Configuration
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<h1>{{ $title }}</h1>
|
||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||
<div class="navbar-main" x-data">
|
||||
<nav class="flex shrink-0 gap-4 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<nav class="flex shrink-0 gap-6 items-center whitespace-nowrap scrollbar min-h-10">
|
||||
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('project.service.configuration', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
|
|
|
|||
|
|
@ -20,25 +20,51 @@
|
|||
<p>A custom health check has been detected. If you enable this health check, it will disable the custom one and use this instead.</p>
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
{{-- Healthcheck Type Selector --}}
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckType" label="Type" required wire:model.live="healthCheckType">
|
||||
<option value="http">HTTP</option>
|
||||
<option value="cmd">CMD</option>
|
||||
</x-forms.select>
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckScheme" label="Scheme" required>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</x-forms.select>
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckHost" placeholder="localhost" label="Host" required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckPort"
|
||||
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckPath" placeholder="/health" label="Path" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckReturnCode" placeholder="200" label="Return Code"
|
||||
required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckResponseText" placeholder="OK" label="Response Text" />
|
||||
</div>
|
||||
|
||||
@if ($healthCheckType === 'http')
|
||||
{{-- HTTP Healthcheck Fields --}}
|
||||
<div class="flex gap-2">
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</x-forms.select>
|
||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckScheme" label="Scheme" required>
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
</x-forms.select>
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckHost" placeholder="localhost" label="Host" required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckPort"
|
||||
helper="If no port is defined, the first exposed port will be used." placeholder="80" label="Port" />
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckPath" placeholder="/health" label="Path" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" type="number" id="healthCheckReturnCode" placeholder="200" label="Return Code"
|
||||
required />
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckResponseText" placeholder="OK" label="Response Text" />
|
||||
</div>
|
||||
@else
|
||||
{{-- CMD Healthcheck Fields --}}
|
||||
<x-callout type="warning" title="Caution">
|
||||
<p>This command runs inside the container on every health check interval. Shell operators (;, |, &, $, >, <) are not allowed.</p>
|
||||
</x-callout>
|
||||
<div class="flex flex-col gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" id="healthCheckCommand"
|
||||
label="Command"
|
||||
placeholder="pg_isready -U postgres"
|
||||
helper="A simple command to run inside the container. Must exit with code 0 on success. Shell operators like ;, |, &&, $() are not allowed."
|
||||
:required="$healthCheckType === 'cmd'" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Common timing fields (used by both types) --}}
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
||||
label="Interval (s)" required />
|
||||
|
|
@ -49,4 +75,4 @@
|
|||
label="Start Period (s)" required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,22 @@
|
|||
<div class="mt-1 mb-6">Configure Docker cleanup settings for your server.</div>
|
||||
</div>
|
||||
|
||||
@if ($this->isCleanupStale)
|
||||
<div class="mb-4">
|
||||
<x-callout type="warning" title="Docker Cleanup May Be Stalled">
|
||||
<p>The last Docker cleanup ran {{ $this->lastExecutionTime ?? 'unknown time' }} ago,
|
||||
which is longer than expected for the configured frequency.</p>
|
||||
@if (!$this->isSchedulerHealthy)
|
||||
<p class="mt-1">The scheduled job manager appears to be inactive. This may indicate
|
||||
a stale Redis lock is blocking all scheduled jobs.</p>
|
||||
@endif
|
||||
<p class="mt-2">To resolve, run on your Coolify instance:
|
||||
<code class="bg-black/10 dark:bg-white/10 px-1 rounded">php artisan cleanup:redis --clear-locks</code>
|
||||
</p>
|
||||
</x-callout>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-4">
|
||||
<h3>Cleanup Configuration</h3>
|
||||
|
|
|
|||
260
resources/views/livewire/settings/scheduled-jobs.blade.php
Normal file
260
resources/views/livewire/settings/scheduled-jobs.blade.php
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
<div>
|
||||
<x-slot:title>
|
||||
Scheduled Job Issues | Coolify
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'executions' }"
|
||||
class="flex flex-col gap-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Scheduled Job Issues</h2>
|
||||
<x-forms.button wire:click="refresh">Refresh</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4">Shows failed executions, skipped jobs, and scheduler health.</div>
|
||||
</div>
|
||||
|
||||
{{-- Tab Buttons --}}
|
||||
<div class="flex flex-row gap-4">
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'executions' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'executions'; window.location.hash = 'executions'">
|
||||
Failures ({{ $executions->count() }})
|
||||
</div>
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'scheduler-runs' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'scheduler-runs'; window.location.hash = 'scheduler-runs'">
|
||||
Scheduler Runs ({{ $managerRuns->count() }})
|
||||
</div>
|
||||
<div @class([
|
||||
'box-without-bg cursor-pointer dark:bg-coolgray-100 dark:text-white w-full text-center items-center justify-center',
|
||||
])
|
||||
:class="activeTab === 'skipped-jobs' && 'dark:bg-coollabs bg-coollabs text-white'"
|
||||
@click="activeTab = 'skipped-jobs'; window.location.hash = 'skipped-jobs'">
|
||||
Skipped Jobs ({{ $skipLogs->count() }})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Executions Tab --}}
|
||||
<div x-show="activeTab === 'executions'" x-cloak>
|
||||
{{-- Filters --}}
|
||||
<div class="flex gap-4 flex-wrap mb-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium">Type</label>
|
||||
<select wire:model.live="filterType"
|
||||
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
|
||||
<option value="all">All Types</option>
|
||||
<option value="backup">Backups</option>
|
||||
<option value="task">Tasks</option>
|
||||
<option value="cleanup">Docker Cleanup</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-sm font-medium">Time Range</label>
|
||||
<select wire:model.live="filterDate"
|
||||
class="w-40 border bg-white dark:bg-coolgray-100 border-gray-300 dark:border-coolgray-400 rounded-md text-sm">
|
||||
<option value="last_24h">Last 24 Hours</option>
|
||||
<option value="last_7d">Last 7 Days</option>
|
||||
<option value="last_30d">Last 30 Days</option>
|
||||
<option value="all">All Time</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Resource</th>
|
||||
<th class="px-4 py-3">Server</th>
|
||||
<th class="px-4 py-3">Started</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($executions as $execution)
|
||||
<tr wire:key="exec-{{ $execution['type'] }}-{{ $execution['id'] }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400 hover:bg-gray-50 dark:hover:bg-coolgray-200">
|
||||
<td class="px-4 py-3">
|
||||
@php
|
||||
$typeLabel = match($execution['type']) {
|
||||
'backup' => 'Backup',
|
||||
'task' => 'Task',
|
||||
'cleanup' => 'Cleanup',
|
||||
default => ucfirst($execution['type']),
|
||||
};
|
||||
$typeBg = match($execution['type']) {
|
||||
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
'cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $typeBg }}">
|
||||
{{ $typeLabel }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
{{ $execution['resource_name'] }}
|
||||
@if($execution['resource_type'])
|
||||
<span class="text-xs text-gray-500">({{ $execution['resource_type'] }})</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3">{{ $execution['server_name'] }}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
{{ $execution['created_at']->diffForHumans() }}
|
||||
<span class="block text-xs text-gray-500">{{ $execution['created_at']->format('M d H:i') }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap">
|
||||
@if($execution['finished_at'] && $execution['created_at'])
|
||||
{{ \Carbon\Carbon::parse($execution['created_at'])->diffInSeconds(\Carbon\Carbon::parse($execution['finished_at'])) }}s
|
||||
@elseif($execution['status'] === 'running')
|
||||
<x-loading class="w-4 h-4" />
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 max-w-xs truncate" title="{{ $execution['message'] }}">
|
||||
{{ \Illuminate\Support\Str::limit($execution['message'], 80) }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500">
|
||||
No failures found for the selected filters.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Scheduler Runs Tab --}}
|
||||
<div x-show="activeTab === 'scheduler-runs'" x-cloak>
|
||||
<div class="pb-4 text-sm text-gray-500">Shows when the ScheduledJobManager executed. Gaps indicate lock conflicts or missed runs.</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Event</th>
|
||||
<th class="px-4 py-3">Duration</th>
|
||||
<th class="px-4 py-3">Dispatched</th>
|
||||
<th class="px-4 py-3">Skipped</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($managerRuns as $run)
|
||||
<tr wire:key="run-{{ $loop->index }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $run['timestamp'] }}</td>
|
||||
<td class="px-4 py-2">{{ $run['message'] }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@if($run['duration_ms'] !== null)
|
||||
{{ $run['duration_ms'] }}ms
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-2">{{ $run['dispatched'] ?? '-' }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@if(($run['skipped'] ?? 0) > 0)
|
||||
<span class="text-warning">{{ $run['skipped'] }}</span>
|
||||
@else
|
||||
{{ $run['skipped'] ?? '-' }}
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-4 text-center text-gray-500">
|
||||
No scheduler run logs found. Logs appear after the ScheduledJobManager runs.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Skipped Jobs Tab --}}
|
||||
<div x-show="activeTab === 'skipped-jobs'" x-cloak>
|
||||
<div class="pb-4 text-sm text-gray-500">Jobs that were not dispatched because conditions were not met.</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs uppercase bg-gray-50 dark:bg-coolgray-200">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Time</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Reason</th>
|
||||
<th class="px-4 py-3">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($skipLogs as $skip)
|
||||
<tr wire:key="skip-{{ $loop->index }}"
|
||||
class="border-b border-gray-200 dark:border-coolgray-400">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-xs">{{ $skip['timestamp'] }}</td>
|
||||
<td class="px-4 py-2">
|
||||
@php
|
||||
$skipTypeBg = match($skip['type']) {
|
||||
'backup' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
'task' => 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
'docker_cleanup' => 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300',
|
||||
};
|
||||
@endphp
|
||||
<span class="px-2 py-1 rounded-md text-xs font-medium {{ $skipTypeBg }}">
|
||||
{{ ucfirst(str_replace('_', ' ', $skip['type'])) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
@php
|
||||
$reasonLabel = match($skip['reason']) {
|
||||
'server_not_functional' => 'Server not functional',
|
||||
'subscription_unpaid' => 'Subscription unpaid',
|
||||
'database_deleted' => 'Database deleted',
|
||||
'server_deleted' => 'Server deleted',
|
||||
'resource_deleted' => 'Resource deleted',
|
||||
'application_not_running' => 'Application not running',
|
||||
'service_not_running' => 'Service not running',
|
||||
default => ucfirst(str_replace('_', ' ', $skip['reason'])),
|
||||
};
|
||||
$reasonBg = match($skip['reason']) {
|
||||
'server_not_functional', 'database_deleted', 'server_deleted', 'resource_deleted' => 'text-red-600 dark:text-red-400',
|
||||
'subscription_unpaid' => 'text-warning',
|
||||
'application_not_running', 'service_not_running' => 'text-orange-600 dark:text-orange-400',
|
||||
default => '',
|
||||
};
|
||||
@endphp
|
||||
<span class="{{ $reasonBg }}">{{ $reasonLabel }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-xs text-gray-500">
|
||||
@php
|
||||
$details = collect($skip['context'])
|
||||
->except(['type', 'skip_reason', 'execution_time'])
|
||||
->map(fn($v, $k) => str_replace('_', ' ', $k) . ': ' . $v)
|
||||
->implode(', ');
|
||||
@endphp
|
||||
{{ $details }}
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-4 text-center text-gray-500">
|
||||
No skipped jobs found. This means all scheduled jobs passed their conditions.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -9,6 +9,7 @@ use App\Http\Controllers\Api\HetznerController;
|
|||
use App\Http\Controllers\Api\OtherController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ResourcesController;
|
||||
use App\Http\Controllers\Api\ScheduledTasksController;
|
||||
use App\Http\Controllers\Api\SecurityController;
|
||||
use App\Http\Controllers\Api\ServersController;
|
||||
use App\Http\Controllers\Api\ServicesController;
|
||||
|
|
@ -54,7 +55,7 @@ Route::group([
|
|||
Route::post('/projects/{uuid}/environments', [ProjectController::class, 'create_environment'])->middleware(['api.ability:write']);
|
||||
Route::delete('/projects/{uuid}/environments/{environment_name_or_uuid}', [ProjectController::class, 'delete_environment'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:read']);
|
||||
Route::post('/projects', [ProjectController::class, 'create_project'])->middleware(['api.ability:write']);
|
||||
Route::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);
|
||||
Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_project'])->middleware(['api.ability:write']);
|
||||
|
||||
|
|
@ -85,7 +86,7 @@ Route::group([
|
|||
|
||||
Route::get('/servers/{uuid}/validate', [ServersController::class, 'validate_server'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:read']);
|
||||
Route::post('/servers', [ServersController::class, 'create_server'])->middleware(['api.ability:write']);
|
||||
Route::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['api.ability:write']);
|
||||
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']);
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ Route::group([
|
|||
|
||||
/**
|
||||
* @deprecated Use POST /api/v1/services instead. This endpoint creates a Service, not an Application and is a unstable duplicate of POST /api/v1/services.
|
||||
*/
|
||||
*/
|
||||
Route::post('/applications/dockercompose', [ApplicationsController::class, 'create_dockercompose_application'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::get('/applications/{uuid}', [ApplicationsController::class, 'application_by_uuid'])->middleware(['api.ability:read']);
|
||||
|
|
@ -120,9 +121,9 @@ Route::group([
|
|||
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::get('/applications/{uuid}/logs', [ApplicationsController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/applications/{uuid}/stop', [ApplicationsController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||
|
||||
Route::get('/github-apps', [GithubController::class, 'list_github_apps'])->middleware(['api.ability:read']);
|
||||
Route::post('/github-apps', [GithubController::class, 'create_github_app'])->middleware(['api.ability:write']);
|
||||
|
|
@ -151,9 +152,9 @@ Route::group([
|
|||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}', [DatabasesController::class, 'delete_backup_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}', [DatabasesController::class, 'delete_execution_by_uuid'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/databases/{uuid}/stop', [DatabasesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||
|
||||
Route::get('/services', [ServicesController::class, 'services'])->middleware(['api.ability:read']);
|
||||
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
|
||||
|
|
@ -168,9 +169,21 @@ Route::group([
|
|||
Route::patch('/services/{uuid}/envs', [ServicesController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/services/{uuid}/envs/{env_uuid}', [ServicesController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
||||
|
||||
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:write']);
|
||||
Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:deploy']);
|
||||
Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware(['api.ability:deploy']);
|
||||
|
||||
Route::get('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_application_uuid'])->middleware(['api.ability:read']);
|
||||
Route::post('/applications/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
|
||||
Route::patch('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/applications/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_application_uuid'])->middleware(['api.ability:write']);
|
||||
Route::get('/applications/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_application_uuid'])->middleware(['api.ability:read']);
|
||||
|
||||
Route::get('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'scheduled_tasks_by_service_uuid'])->middleware(['api.ability:read']);
|
||||
Route::post('/services/{uuid}/scheduled-tasks', [ScheduledTasksController::class, 'create_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
|
||||
Route::patch('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'update_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
|
||||
Route::delete('/services/{uuid}/scheduled-tasks/{task_uuid}', [ScheduledTasksController::class, 'delete_scheduled_task_by_service_uuid'])->middleware(['api.ability:write']);
|
||||
Route::get('/services/{uuid}/scheduled-tasks/{task_uuid}/executions', [ScheduledTasksController::class, 'executions_by_service_uuid'])->middleware(['api.ability:read']);
|
||||
});
|
||||
|
||||
Route::group([
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ use App\Livewire\Server\Show as ServerShow;
|
|||
use App\Livewire\Server\Swarm as ServerSwarm;
|
||||
use App\Livewire\Settings\Advanced as SettingsAdvanced;
|
||||
use App\Livewire\Settings\Index as SettingsIndex;
|
||||
use App\Livewire\Settings\ScheduledJobs as SettingsScheduledJobs;
|
||||
use App\Livewire\Settings\Updates as SettingsUpdates;
|
||||
use App\Livewire\SettingsBackup;
|
||||
use App\Livewire\SettingsEmail;
|
||||
|
|
@ -119,6 +120,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||
Route::get('/settings/backup', SettingsBackup::class)->name('settings.backup');
|
||||
Route::get('/settings/email', SettingsEmail::class)->name('settings.email');
|
||||
Route::get('/settings/oauth', SettingsOauth::class)->name('settings.oauth');
|
||||
Route::get('/settings/scheduled-jobs', SettingsScheduledJobs::class)->name('settings.scheduled-jobs');
|
||||
|
||||
Route::get('/profile', ProfileIndex::class)->name('profile');
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,26 @@
|
|||
|
||||
services:
|
||||
beszel-agent:
|
||||
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
|
||||
image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
|
||||
network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
|
||||
environment:
|
||||
# Required
|
||||
- LISTEN=/beszel_socket/beszel.sock
|
||||
- HUB_URL=${HUB_URL?}
|
||||
- 'TOKEN=${TOKEN?}'
|
||||
- 'KEY=${KEY?}'
|
||||
- HUB_URL=$SERVICE_URL_BESZEL
|
||||
- TOKEN=${TOKEN} # From hub token settings
|
||||
- KEY=${KEY} # SSH public key(s) from hub
|
||||
# Optional
|
||||
- DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
|
||||
- LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
|
||||
- SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
|
||||
- SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
|
||||
volumes:
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
healthcheck:
|
||||
test: ['CMD', '/agent', 'health']
|
||||
interval: 60s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
|
|
@ -9,21 +9,41 @@
|
|||
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
|
||||
services:
|
||||
beszel:
|
||||
image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025
|
||||
image: 'henrygd/beszel:0.18.4' # Released on 21 Feb 2026
|
||||
environment:
|
||||
- SERVICE_URL_BESZEL_8090
|
||||
- CONTAINER_DETAILS=${CONTAINER_DETAILS:-true}
|
||||
- SHARE_ALL_SYSTEMS=${SHARE_ALL_SYSTEMS:-false}
|
||||
volumes:
|
||||
- 'beszel_data:/beszel_data'
|
||||
- 'beszel_socket:/beszel_socket'
|
||||
healthcheck:
|
||||
test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
beszel-agent:
|
||||
image: 'henrygd/beszel-agent:0.16.1' # Released on 14 Nov 2025
|
||||
image: 'henrygd/beszel-agent:0.18.4' # Released on 21 Feb 2026
|
||||
network_mode: host # Network stats graphs won't work if agent cannot access host system network stack
|
||||
environment:
|
||||
# Required
|
||||
- LISTEN=/beszel_socket/beszel.sock
|
||||
- HUB_URL=http://beszel:8090
|
||||
- 'TOKEN=${TOKEN}'
|
||||
- 'KEY=${KEY}'
|
||||
- HUB_URL=$SERVICE_URL_BESZEL
|
||||
- TOKEN=${TOKEN} # From hub token settings
|
||||
- KEY=${KEY} # SSH public key(s) from hub
|
||||
# Optional
|
||||
- DISABLE_SSH=${DISABLE_SSH:-false} # Disable SSH
|
||||
- LOG_LEVEL=${LOG_LEVEL:-warn} # Logging level
|
||||
- SKIP_GPU=${SKIP_GPU:-false} # Skip GPU monitoring
|
||||
- SYSTEM_NAME=${SYSTEM_NAME} # Custom system name
|
||||
volumes:
|
||||
- beszel_agent_data:/var/lib/beszel-agent
|
||||
- beszel_socket:/beszel_socket
|
||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
||||
|
||||
healthcheck:
|
||||
test: ['CMD', '/agent', 'health']
|
||||
interval: 60s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
start_period: 5s
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# documentation: https://glitchtip.com
|
||||
# slogan: GlitchTip is a self-hosted, open-source error tracking system.
|
||||
# slogan: GlitchTip is a error tracking system.
|
||||
# category: monitoring
|
||||
# tags: error, tracking, open-source, self-hosted, sentry
|
||||
# tags: error, tracking, sentry
|
||||
# logo: svgs/glitchtip.png
|
||||
# port: 8080
|
||||
# port: 8000
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
@ -29,14 +29,14 @@ services:
|
|||
retries: 10
|
||||
|
||||
web:
|
||||
image: glitchtip/glitchtip
|
||||
image: glitchtip/glitchtip:6.0
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- SERVICE_URL_GLITCHTIP_8080
|
||||
- SERVICE_URL_GLITCHTIP_8000
|
||||
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip}
|
||||
- SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION
|
||||
- EMAIL_URL=${EMAIL_URL:-consolemail://}
|
||||
|
|
@ -53,7 +53,7 @@ services:
|
|||
retries: 10
|
||||
|
||||
worker:
|
||||
image: glitchtip/glitchtip
|
||||
image: glitchtip/glitchtip:6.0
|
||||
command: ./bin/run-celery-with-beat.sh
|
||||
depends_on:
|
||||
postgres:
|
||||
|
|
@ -77,7 +77,7 @@ services:
|
|||
retries: 10
|
||||
|
||||
migrate:
|
||||
image: glitchtip/glitchtip
|
||||
image: glitchtip/glitchtip:6.0
|
||||
restart: "no"
|
||||
depends_on:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# ignore: true
|
||||
# documentation: https://docs.plane.so/self-hosting/methods/docker-compose
|
||||
# slogan: The open source project management tool
|
||||
# category: productivity
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# ignore: true
|
||||
# documentation: https://pterodactyl.io/
|
||||
# slogan: Pterodactyl is a free, open-source game server management panel
|
||||
# category: media
|
||||
|
|
@ -102,4 +103,4 @@ services:
|
|||
- MAIL_PORT=$MAIL_PORT
|
||||
- MAIL_USERNAME=$MAIL_USERNAME
|
||||
- MAIL_PASSWORD=$MAIL_PASSWORD
|
||||
- MAIL_ENCRYPTION=$MAIL_ENCRYPTION
|
||||
- MAIL_ENCRYPTION=$MAIL_ENCRYPTION
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# ignore: true
|
||||
# documentation: https://pterodactyl.io/
|
||||
# slogan: Pterodactyl is a free, open-source game server management panel
|
||||
# category: media
|
||||
|
|
|
|||
31
templates/compose/spacebot.yaml
Normal file
31
templates/compose/spacebot.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# documentation: https://docs.spacebot.sh/docker
|
||||
# slogan: An agentic AI system with specialized processes for thinking, working, and remembering.
|
||||
# category: ai
|
||||
# tags: ai, agent, anthropic, openai, discord, slack, llm, agentic
|
||||
# logo: svgs/spacebot.png
|
||||
# port: 19898
|
||||
|
||||
services:
|
||||
spacebot:
|
||||
image: "ghcr.io/spacedriveapp/spacebot:full"
|
||||
environment:
|
||||
- SERVICE_FQDN_SPACEBOT_19898
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY}
|
||||
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
|
||||
- SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN}
|
||||
- SLACK_APP_TOKEN=${SLACK_APP_TOKEN}
|
||||
- BRAVE_SEARCH_API_KEY=${BRAVE_SEARCH_API_KEY}
|
||||
- SPACEBOT_CHANNEL_MODEL=${SPACEBOT_CHANNEL_MODEL}
|
||||
- SPACEBOT_WORKER_MODEL=${SPACEBOT_WORKER_MODEL}
|
||||
volumes:
|
||||
- "spacebot-data:/data"
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
shm_size: 1g
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-sf", "http://localhost:19898/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
tests/Browser/screenshots/.gitignore
vendored
2
tests/Browser/screenshots/.gitignore
vendored
|
|
@ -1,2 +0,0 @@
|
|||
*
|
||||
!.gitignore
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue