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.
|
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.
|
## Running Tests
|
||||||
- Browser tests: `tests/Browser/` directory.
|
|
||||||
- Do NOT remove tests without approval - these are core application code.
|
|
||||||
|
|
||||||
### 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">
|
<code-snippet name="Basic Pest Test Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -45,24 +54,10 @@ it('is true', function () {
|
||||||
|
|
||||||
</code-snippet>
|
</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
|
## Assertions
|
||||||
|
|
||||||
Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
|
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 |
|
| Use | Instead of |
|
||||||
|-----|------------|
|
|-----|------------|
|
||||||
| `assertSuccessful()` | `assertStatus(200)` |
|
| `assertSuccessful()` | `assertStatus(200)` |
|
||||||
|
|
@ -75,7 +70,7 @@ Import mock function before use: `use function Pest\Laravel\mock;`
|
||||||
|
|
||||||
## Datasets
|
## Datasets
|
||||||
|
|
||||||
Use datasets for repetitive tests (validation rules, etc.):
|
Use datasets for repetitive tests:
|
||||||
|
|
||||||
<code-snippet name="Pest Dataset Example" lang="php">
|
<code-snippet name="Pest Dataset Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -88,73 +83,94 @@ it('has emails', function (string $email) {
|
||||||
|
|
||||||
</code-snippet>
|
</code-snippet>
|
||||||
|
|
||||||
## Pest 4 Features
|
## Browser Testing (Pest Browser Plugin + Playwright)
|
||||||
|
|
||||||
| Feature | Purpose |
|
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 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 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/`.
|
### Browser Test Template
|
||||||
- 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.
|
|
||||||
|
|
||||||
<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 () {
|
use App\Models\InstanceSettings;
|
||||||
Notification::fake();
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
$this->actingAs(User::factory()->create());
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
$page = visit('/sign-in');
|
beforeEach(function () {
|
||||||
|
InstanceSettings::create(['id' => 0]);
|
||||||
$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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can visit the page', function () {
|
||||||
|
$page = visit('/login');
|
||||||
|
|
||||||
|
$page->assertSee('Login');
|
||||||
|
});
|
||||||
</code-snippet>
|
</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">
|
$page = visit('/login');
|
||||||
|
|
||||||
$pages = visit(['/', '/about', '/contact']);
|
|
||||||
|
|
||||||
$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
|
|
||||||
|
|
||||||
|
$page->fill('email', 'random@email.com')
|
||||||
|
->fill('password', 'wrongpassword123')
|
||||||
|
->click('Login')
|
||||||
|
->assertSee('These credentials do not match our records');
|
||||||
|
});
|
||||||
</code-snippet>
|
</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">
|
<code-snippet name="Architecture Test Example" lang="php">
|
||||||
|
|
||||||
|
|
@ -172,3 +188,6 @@ arch('controllers')
|
||||||
- Forgetting datasets for repetitive validation tests
|
- Forgetting datasets for repetitive validation tests
|
||||||
- Deleting tests without approval
|
- 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-helper-next.yml
|
||||||
- .github/workflows/coolify-realtime.yml
|
- .github/workflows/coolify-realtime.yml
|
||||||
- .github/workflows/coolify-realtime-next.yml
|
- .github/workflows/coolify-realtime-next.yml
|
||||||
|
- .github/workflows/pr-quality.yaml
|
||||||
- docker/coolify-helper/Dockerfile
|
- docker/coolify-helper/Dockerfile
|
||||||
- docker/coolify-realtime/Dockerfile
|
- docker/coolify-realtime/Dockerfile
|
||||||
- docker/testing-host/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-helper-next.yml
|
||||||
- .github/workflows/coolify-realtime.yml
|
- .github/workflows/coolify-realtime.yml
|
||||||
- .github/workflows/coolify-realtime-next.yml
|
- .github/workflows/coolify-realtime-next.yml
|
||||||
|
- .github/workflows/pr-quality.yaml
|
||||||
- docker/coolify-helper/Dockerfile
|
- docker/coolify-helper/Dockerfile
|
||||||
- docker/coolify-realtime/Dockerfile
|
- docker/coolify-realtime/Dockerfile
|
||||||
- docker/testing-host/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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ v4.x ]
|
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:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
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
|
.DS_Store
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
/.workspaces
|
/.workspaces
|
||||||
|
tests/Browser/Screenshots
|
||||||
|
tests/v4/Browser/Screenshots
|
||||||
|
|
|
||||||
|
|
@ -112,12 +112,52 @@ class StartDatabaseProxy
|
||||||
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
|
||||||
$nginxconf_base64 = base64_encode($nginxconf);
|
$nginxconf_base64 = base64_encode($nginxconf);
|
||||||
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
|
instant_remote_process(["docker rm -f $proxyContainerName"], $server, false);
|
||||||
instant_remote_process([
|
|
||||||
"mkdir -p $configuration_dir",
|
try {
|
||||||
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
instant_remote_process([
|
||||||
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
"mkdir -p $configuration_dir",
|
||||||
"docker compose --project-directory {$configuration_dir} pull",
|
"echo '{$nginxconf_base64}' | base64 -d | tee $configuration_dir/nginx.conf > /dev/null",
|
||||||
"docker compose --project-directory {$configuration_dir} up -d",
|
"echo '{$dockercompose_base64}' | base64 -d | tee $configuration_dir/docker-compose.yaml > /dev/null",
|
||||||
], $server);
|
"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) {
|
if ($this->database->enable_ssl) {
|
||||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
$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 stop -t 10 $container_name 2>/dev/null || true";
|
||||||
$this->commands[] = "docker rm -f $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";
|
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,9 @@ class StartRedis
|
||||||
if ($this->database->enable_ssl) {
|
if ($this->database->enable_ssl) {
|
||||||
$this->commands[] = "chown -R 999:999 $this->configuration_dir/ssl/server.key $this->configuration_dir/ssl/server.crt";
|
$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 stop -t 10 $container_name 2>/dev/null || true";
|
||||||
$this->commands[] = "docker rm -f $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";
|
$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/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($serverCert->ssl_certificate);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
remote_process($commands, $server);
|
remote_process($commands, $server);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ class OpenApi extends Command
|
||||||
echo $process->output();
|
echo $process->output();
|
||||||
|
|
||||||
$yaml = file_get_contents('openapi.yaml');
|
$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);
|
file_put_contents('openapi.json', $json);
|
||||||
echo "Converted OpenAPI YAML to JSON.\n";
|
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->job(new CleanupStaleMultiplexedConnections)->hourly();
|
||||||
$this->scheduleInstance->command('cleanup:redis')->weekly();
|
$this->scheduleInstance->command('cleanup:redis --clear-locks')->daily();
|
||||||
|
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
// Instance Jobs
|
// Instance Jobs
|
||||||
|
|
|
||||||
|
|
@ -1002,7 +1002,7 @@ class ApplicationsController extends Controller
|
||||||
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
if ($return instanceof \Illuminate\Http\JsonResponse) {
|
||||||
return $return;
|
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(), [
|
$validator = customApiValidator($request->all(), [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
|
|
@ -1101,7 +1101,6 @@ class ApplicationsController extends Controller
|
||||||
'git_branch' => ['string', 'required', new ValidGitBranch],
|
'git_branch' => ['string', 'required', new ValidGitBranch],
|
||||||
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
'build_pack' => ['required', Rule::enum(BuildPackTypes::class)],
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'docker_compose_location' => 'string',
|
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
|
|
@ -1297,7 +1296,6 @@ class ApplicationsController extends Controller
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'github_app_uuid' => 'string|required',
|
'github_app_uuid' => 'string|required',
|
||||||
'watch_paths' => 'string|nullable',
|
'watch_paths' => 'string|nullable',
|
||||||
'docker_compose_location' => 'string',
|
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
|
|
@ -1525,7 +1523,6 @@ class ApplicationsController extends Controller
|
||||||
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
|
||||||
'private_key_uuid' => 'string|required',
|
'private_key_uuid' => 'string|required',
|
||||||
'watch_paths' => 'string|nullable',
|
'watch_paths' => 'string|nullable',
|
||||||
'docker_compose_location' => 'string',
|
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
|
|
@ -2463,14 +2460,13 @@ class ApplicationsController extends Controller
|
||||||
$this->authorize('update', $application);
|
$this->authorize('update', $application);
|
||||||
|
|
||||||
$server = $application->destination->server;
|
$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 = [
|
$validationRules = [
|
||||||
'name' => 'string|max:255',
|
'name' => 'string|max:255',
|
||||||
'description' => 'string|nullable',
|
'description' => 'string|nullable',
|
||||||
'static_image' => 'string',
|
'static_image' => 'string',
|
||||||
'watch_paths' => 'string|nullable',
|
'watch_paths' => 'string|nullable',
|
||||||
'docker_compose_location' => 'string',
|
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_domains.*' => 'array:name,domain',
|
'docker_compose_domains.*' => 'array:name,domain',
|
||||||
'docker_compose_domains.*.name' => 'string|required',
|
'docker_compose_domains.*.name' => 'string|required',
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,10 @@ class DeployController extends Controller
|
||||||
if (! $deployment) {
|
if (! $deployment) {
|
||||||
return response()->json(['message' => 'Deployment not found.'], 404);
|
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));
|
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');
|
$uuid = $request->get('uuid');
|
||||||
if ($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();
|
$projects = Project::where('team_id', $teamId)->get();
|
||||||
$domains = collect();
|
$domains = collect();
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class CheckForcePasswordReset
|
||||||
}
|
}
|
||||||
$force_password_reset = auth()->user()->force_password_reset;
|
$force_password_reset = auth()->user()->force_password_reset;
|
||||||
if ($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);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
|
||||||
private bool $dockerBuildkitSupported = false;
|
private bool $dockerBuildkitSupported = false;
|
||||||
|
|
||||||
|
private bool $dockerSecretsSupported = false;
|
||||||
|
|
||||||
private bool $skip_build = false;
|
private bool $skip_build = false;
|
||||||
|
|
||||||
private Collection|string $build_secrets;
|
private Collection|string $build_secrets;
|
||||||
|
|
@ -251,7 +253,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
}
|
}
|
||||||
if ($this->application->build_pack === 'dockerfile') {
|
if ($this->application->build_pack === 'dockerfile') {
|
||||||
if (data_get($this->application, 'dockerfile_location')) {
|
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
|
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;
|
$serverToCheck = $this->use_build_server ? $this->build_server : $this->server;
|
||||||
$serverName = $this->use_build_server ? "build server ({$serverToCheck->name})" : "deployment server ({$serverToCheck->name})";
|
$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)) {
|
if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) {
|
||||||
$this->dockerBuildkitSupported = false;
|
$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;
|
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'"],
|
["docker buildx version >/dev/null 2>&1 && echo 'available' || echo 'not-available'"],
|
||||||
$serverToCheck
|
$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(
|
$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
|
$serverToCheck
|
||||||
);
|
);
|
||||||
|
|
||||||
if (trim($buildkitTest) === 'supported') {
|
if (trim($buildkitTest) === 'supported') {
|
||||||
$this->dockerBuildkitSupported = true;
|
$this->dockerBuildkitSupported = true;
|
||||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit secrets support detected on {$serverName}.");
|
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit support detected on {$serverName}.");
|
||||||
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
|
|
||||||
} else {
|
} else {
|
||||||
$this->dockerBuildkitSupported = false;
|
$this->dockerBuildkitSupported = false;
|
||||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not have BuildKit secrets support.");
|
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit. Build output progress will be limited.");
|
||||||
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
|
|
||||||
}
|
}
|
||||||
} 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(
|
$secretsTest = instant_remote_process(
|
||||||
["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
|
["docker build --help 2>&1 | grep -q 'secret' && echo 'supported' || echo 'not-supported'"],
|
||||||
$serverToCheck
|
$serverToCheck
|
||||||
);
|
);
|
||||||
|
|
||||||
if (trim($secretsTest) === 'supported') {
|
if (trim($secretsTest) === 'supported') {
|
||||||
$this->dockerBuildkitSupported = true;
|
$this->dockerSecretsSupported = true;
|
||||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}.");
|
|
||||||
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
|
$this->application_deployment_queue->addLogEntry('Build secrets are enabled and will be used for enhanced security.');
|
||||||
} else {
|
} else {
|
||||||
$this->dockerBuildkitSupported = false;
|
$this->dockerSecretsSupported = false;
|
||||||
$this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with Buildx on {$serverName}, but secrets not supported.");
|
$this->application_deployment_queue->addLogEntry("Docker on {$serverName} does not support build secrets. Using traditional build arguments.");
|
||||||
$this->application_deployment_queue->addLogEntry('Build secrets feature is enabled but not supported. Using traditional build arguments.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->dockerBuildkitSupported = false;
|
$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("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()
|
private function deploy_docker_compose_buildpack()
|
||||||
{
|
{
|
||||||
if (data_get($this->application, 'docker_compose_location')) {
|
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')) {
|
if (data_get($this->application, 'docker_compose_custom_start_command')) {
|
||||||
$this->docker_compose_custom_start_command = $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
|
// For raw compose, we cannot automatically add secrets configuration
|
||||||
// User must define it manually in their docker-compose file
|
// 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.');
|
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -653,7 +650,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add build secrets to compose file if enabled and BuildKit is supported
|
// 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);
|
$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
|
// 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()) {
|
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(' ');
|
$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)
|
// Inject build args right after 'build' subcommand (not at the end)
|
||||||
$original_command = $build_command;
|
$original_command = $build_command;
|
||||||
|
|
@ -702,9 +697,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->execute_remote_command(
|
try {
|
||||||
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$build_command}"), 'hidden' => true],
|
$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 {
|
} else {
|
||||||
$command = "{$this->coolify_variables} docker compose";
|
$command = "{$this->coolify_variables} docker compose";
|
||||||
// Prepend DOCKER_BUILDKIT=1 if BuildKit is supported
|
// 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()) {
|
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(' ');
|
$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}";
|
$command .= " {$build_args_string}";
|
||||||
$this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
|
$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->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 {
|
} else {
|
||||||
$this->write_deployment_configurations();
|
$this->write_deployment_configurations();
|
||||||
$this->docker_compose_location = '/docker-compose.yaml';
|
$this->docker_compose_location = '/docker-compose.yaml';
|
||||||
|
|
@ -831,7 +841,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
$this->server = $this->build_server;
|
$this->server = $this->build_server;
|
||||||
}
|
}
|
||||||
if (data_get($this->application, 'dockerfile_location')) {
|
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->prepare_builder_image();
|
||||||
$this->check_git_if_build_needed();
|
$this->check_git_if_build_needed();
|
||||||
|
|
@ -1800,7 +1810,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
$counter = 1;
|
$counter = 1;
|
||||||
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
$this->application_deployment_queue->addLogEntry('Waiting for healthcheck to pass on the new container.');
|
||||||
if ($this->full_healthcheck_url && ! $this->application->custom_healthcheck_found) {
|
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.");
|
$this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
|
||||||
$sleeptime = 0;
|
$sleeptime = 0;
|
||||||
|
|
@ -2758,29 +2769,55 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
|
||||||
private function generate_healthcheck_commands()
|
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) {
|
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 {
|
} 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') {
|
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
|
||||||
$health_check_port = 80;
|
$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}";
|
$method = $this->sanitizeHealthCheckValue($this->application->health_check_method, '/^[A-Z]+$/', 'GET');
|
||||||
$generated_healthchecks_commands = [
|
$scheme = $this->sanitizeHealthCheckValue($this->application->health_check_scheme, '/^https?$/', 'http');
|
||||||
"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",
|
$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 {
|
} else {
|
||||||
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
|
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$scheme}://{$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",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$generated_healthchecks_commands = [
|
||||||
|
"curl -s -X {$method} -f {$url} > /dev/null || wget -q -O- {$url} > /dev/null || exit 1",
|
||||||
|
];
|
||||||
|
|
||||||
return implode(' ', $generated_healthchecks_commands);
|
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)
|
private function pull_latest_image($image)
|
||||||
{
|
{
|
||||||
$this->application_deployment_queue->addLogEntry("Pulling latest image ($image) from the registry.");
|
$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());
|
$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);
|
$base64_build_command = base64_encode($build_command);
|
||||||
$this->execute_remote_command(
|
$this->execute_remote_command(
|
||||||
[
|
[
|
||||||
|
|
@ -2857,21 +2898,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
private function build_image()
|
private function build_image()
|
||||||
{
|
{
|
||||||
// Add Coolify related variables to the build args/secrets
|
// Add Coolify related variables to the build args/secrets
|
||||||
if ($this->dockerBuildkitSupported) {
|
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 {
|
|
||||||
// Traditional build args approach - generate COOLIFY_ variables locally
|
// 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 = $this->generate_coolify_env_variables(forBuildTime: true);
|
||||||
$coolify_envs->each(function ($value, $key) {
|
$coolify_envs->each(function ($value, $key) {
|
||||||
$this->build_args->push("--build-arg '{$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('----------------------------------------');
|
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||||
if ($this->disableBuildCache) {
|
if ($this->disableBuildCache) {
|
||||||
$this->application_deployment_queue->addLogEntry('Docker build cache is disabled. It will not be used during the build process.');
|
$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"),
|
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
]);
|
]);
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the nixpacks Dockerfile to use build secrets
|
// Modify the nixpacks Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||||
|
|
@ -2907,9 +2946,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
} elseif ($this->dockerBuildkitSupported) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// 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}");
|
$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 {
|
} 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 {
|
} else {
|
||||||
$this->execute_remote_command([
|
$this->execute_remote_command([
|
||||||
|
|
@ -2919,18 +2957,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
]);
|
]);
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the nixpacks Dockerfile to use build secrets
|
// Modify the nixpacks Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$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}");
|
$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) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// BuildKit without secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$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}");
|
||||||
$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}");
|
|
||||||
} else {
|
} 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]);
|
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
|
||||||
} else {
|
} else {
|
||||||
// Dockerfile buildpack
|
// Dockerfile buildpack
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the Dockerfile to use build secrets
|
// Modify the Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||||
|
|
@ -2963,19 +2999,17 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
}
|
}
|
||||||
} elseif ($this->dockerBuildkitSupported) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// 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) {
|
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 {
|
} 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 {
|
} else {
|
||||||
// Traditional build with args
|
// Traditional build with args
|
||||||
if ($this->force_rebuild) {
|
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 {
|
} 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);
|
$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());
|
$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);
|
$base64_build_command = base64_encode($build_command);
|
||||||
$this->execute_remote_command(
|
$this->execute_remote_command(
|
||||||
[
|
[
|
||||||
|
|
@ -3035,7 +3073,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||||
} else {
|
} else {
|
||||||
// Pure Dockerfile based deployment
|
// Pure Dockerfile based deployment
|
||||||
if ($this->application->dockerfile) {
|
if ($this->application->dockerfile) {
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the Dockerfile to use build secrets
|
// Modify the Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
||||||
|
|
@ -3044,12 +3082,19 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||||
} else {
|
} 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}";
|
$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 {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// Traditional build with args
|
// BuildKit without secrets
|
||||||
if ($this->force_rebuild) {
|
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 {
|
} 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);
|
$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"),
|
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
]);
|
]);
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the nixpacks Dockerfile to use build secrets
|
// Modify the nixpacks Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$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}");
|
$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) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// BuildKit without secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$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}");
|
||||||
$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}");
|
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
$this->execute_remote_command([
|
$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"),
|
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/.nixpacks/Dockerfile"),
|
||||||
'hidden' => true,
|
'hidden' => true,
|
||||||
]);
|
]);
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the nixpacks Dockerfile to use build secrets
|
// Modify the nixpacks Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
||||||
$secrets_flags = $this->build_secrets ? " {$this->build_secrets}" : '';
|
$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}");
|
$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) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// BuildKit without secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}/.nixpacks/Dockerfile");
|
$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}");
|
||||||
$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}");
|
|
||||||
} else {
|
} 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);
|
$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]);
|
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm '.self::NIXPACKS_PLAN_PATH), 'hidden' => true]);
|
||||||
} else {
|
} else {
|
||||||
// Dockerfile buildpack
|
// Dockerfile buildpack
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
// Modify the Dockerfile to use build secrets
|
// Modify the Dockerfile to use build secrets
|
||||||
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
$this->modify_dockerfile_for_secrets("{$this->workdir}{$this->dockerfile_location}");
|
||||||
// Use BuildKit with secrets
|
// Use BuildKit with secrets
|
||||||
|
|
@ -3144,19 +3185,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||||
}
|
}
|
||||||
} elseif ($this->dockerBuildkitSupported) {
|
} elseif ($this->dockerBuildkitSupported) {
|
||||||
// BuildKit without secrets
|
// 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) {
|
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 {
|
} 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 {
|
} else {
|
||||||
// Traditional build with args
|
// Traditional build with args
|
||||||
if ($this->force_rebuild) {
|
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 {
|
} 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);
|
$base64_build_command = base64_encode($build_command);
|
||||||
|
|
@ -3332,7 +3371,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||||
$this->analyzeBuildTimeVariables($variables);
|
$this->analyzeBuildTimeVariables($variables);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerSecretsSupported) {
|
||||||
$this->generate_build_secrets($variables);
|
$this->generate_build_secrets($variables);
|
||||||
$this->build_args = '';
|
$this->build_args = '';
|
||||||
} else {
|
} 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.");
|
$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}";
|
$fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
|
||||||
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
|
$this->modify_dockerfile_for_secrets($fullDockerfilePath);
|
||||||
$this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
|
$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;
|
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()
|
private function run_pre_deployment_command()
|
||||||
{
|
{
|
||||||
if (empty($this->application->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');
|
$serviceId = $labels->get('coolify.serviceId');
|
||||||
$subType = $labels->get('coolify.service.subType');
|
$subType = $labels->get('coolify.service.subType');
|
||||||
$subId = $labels->get('coolify.service.subId');
|
$subId = $labels->get('coolify.service.subId');
|
||||||
|
if (empty($subId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($subType === 'application') {
|
if ($subType === 'application') {
|
||||||
$this->foundServiceApplicationIds->push($subId);
|
$this->foundServiceApplicationIds->push($subId);
|
||||||
// Store container status for aggregation
|
// Store container status for aggregation
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
class ScheduledJobManager implements ShouldQueue
|
class ScheduledJobManager implements ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -27,6 +29,10 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
private ?Carbon $executionTime = null;
|
private ?Carbon $executionTime = null;
|
||||||
|
|
||||||
|
private int $dispatchedCount = 0;
|
||||||
|
|
||||||
|
private int $skippedCount = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new job instance.
|
* Create a new job instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -50,6 +56,11 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function middleware(): array
|
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 [
|
return [
|
||||||
(new WithoutOverlapping('scheduled-job-manager'))
|
(new WithoutOverlapping('scheduled-job-manager'))
|
||||||
->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks
|
->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
|
public function handle(): void
|
||||||
{
|
{
|
||||||
// Freeze the execution time at the start of the job
|
// Freeze the execution time at the start of the job
|
||||||
$this->executionTime = Carbon::now();
|
$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
|
// Process backups - don't let failures stop task processing
|
||||||
try {
|
try {
|
||||||
|
|
@ -91,6 +136,20 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
'trace' => $e->getTraceAsString(),
|
'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
|
private function processScheduledBackups(): void
|
||||||
|
|
@ -101,8 +160,16 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
|
|
||||||
foreach ($backups as $backup) {
|
foreach ($backups as $backup) {
|
||||||
try {
|
try {
|
||||||
// Apply the same filtering logic as the original
|
$skipReason = $this->getBackupSkipReason($backup);
|
||||||
if (! $this->shouldProcessBackup($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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,6 +187,14 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
|
|
||||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||||
DatabaseBackupJob::dispatch($backup);
|
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) {
|
} catch (\Exception $e) {
|
||||||
Log::channel('scheduled-errors')->error('Error processing backup', [
|
Log::channel('scheduled-errors')->error('Error processing backup', [
|
||||||
|
|
@ -138,7 +213,15 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
|
|
||||||
foreach ($tasks as $task) {
|
foreach ($tasks as $task) {
|
||||||
try {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,6 +239,13 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
|
|
||||||
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
if ($this->shouldRunNow($frequency, $serverTimezone)) {
|
||||||
ScheduledTaskJob::dispatch($task);
|
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) {
|
} catch (\Exception $e) {
|
||||||
Log::channel('scheduled-errors')->error('Error processing task', [
|
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'))) {
|
if (blank(data_get($backup, 'database'))) {
|
||||||
$backup->delete();
|
$backup->delete();
|
||||||
|
|
||||||
return false;
|
return 'database_deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
$server = $backup->server();
|
$server = $backup->server();
|
||||||
if (blank($server)) {
|
if (blank($server)) {
|
||||||
$backup->delete();
|
$backup->delete();
|
||||||
|
|
||||||
return false;
|
return 'server_deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($server->isFunctional() === false) {
|
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) {
|
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;
|
$service = $task->service;
|
||||||
$application = $task->application;
|
$application = $task->application;
|
||||||
|
|
@ -201,32 +291,32 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
if (blank($server)) {
|
if (blank($server)) {
|
||||||
$task->delete();
|
$task->delete();
|
||||||
|
|
||||||
return false;
|
return 'server_deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($server->isFunctional() === false) {
|
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) {
|
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
|
||||||
return false;
|
return 'subscription_unpaid';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $service && ! $application) {
|
if (! $service && ! $application) {
|
||||||
$task->delete();
|
$task->delete();
|
||||||
|
|
||||||
return false;
|
return 'resource_deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($application && str($application->status)->contains('running') === false) {
|
if ($application && str($application->status)->contains('running') === false) {
|
||||||
return false;
|
return 'application_not_running';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($service && str($service->status)->contains('running') === false) {
|
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
|
private function shouldRunNow(string $frequency, string $timezone): bool
|
||||||
|
|
@ -248,7 +338,15 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
|
|
||||||
foreach ($servers as $server) {
|
foreach ($servers as $server) {
|
||||||
try {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,6 +368,12 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
$server->settings->delete_unused_volumes,
|
$server->settings->delete_unused_volumes,
|
||||||
$server->settings->delete_unused_networks
|
$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) {
|
} catch (\Exception $e) {
|
||||||
Log::channel('scheduled-errors')->error('Error processing docker cleanup', [
|
Log::channel('scheduled-errors')->error('Error processing docker cleanup', [
|
||||||
|
|
@ -296,19 +400,28 @@ class ScheduledJobManager implements ShouldQueue
|
||||||
return $query->get();
|
return $query->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function shouldProcessDockerCleanup(Server $server): bool
|
private function getDockerCleanupSkipReason(Server $server): ?string
|
||||||
{
|
{
|
||||||
if (! $server->isFunctional()) {
|
if (! $server->isFunctional()) {
|
||||||
return false;
|
return 'server_not_functional';
|
||||||
}
|
}
|
||||||
|
|
||||||
// In cloud, check subscription status (except team 0)
|
// In cloud, check subscription status (except team 0)
|
||||||
if (isCloud() && $server->team_id !== 0) {
|
if (isCloud() && $server->team_id !== 0) {
|
||||||
if (data_get($server->team->subscription, 'stripe_invoice_paid', false) === false) {
|
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\InteractsWithQueue;
|
||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
{
|
{
|
||||||
|
|
@ -33,6 +34,19 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
|
|
||||||
public function __construct(public Server $server) {}
|
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()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -101,12 +101,31 @@ class ServerConnectionCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
'is_usable' => false,
|
'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
|
private function checkHetznerStatus(): void
|
||||||
{
|
{
|
||||||
|
$status = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token);
|
$hetznerService = new \App\Services\HetznerService($this->server->cloudProviderToken->token);
|
||||||
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
|
$serverData = $hetznerService->getServer($this->server->hetzner_server_id);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Laravel\Horizon\Contracts\Silenced;
|
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 __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()
|
public function handle()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1495,6 +1495,7 @@ class GlobalSearch extends Component
|
||||||
'type' => 'one-click-service-'.$serviceKey,
|
'type' => 'one-click-service-'.$serviceKey,
|
||||||
'category' => 'Services',
|
'category' => 'Services',
|
||||||
'resourceType' => 'service',
|
'resourceType' => 'service',
|
||||||
|
'logo' => data_get($service, 'logo'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class General extends Component
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable'])]
|
||||||
public ?string $dockerfile = null;
|
public ?string $dockerfile = null;
|
||||||
|
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])]
|
||||||
public ?string $dockerfileLocation = null;
|
public ?string $dockerfileLocation = null;
|
||||||
|
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable'])]
|
||||||
|
|
@ -85,7 +85,7 @@ class General extends Component
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable'])]
|
||||||
public ?string $dockerRegistryImageTag = null;
|
public ?string $dockerRegistryImageTag = null;
|
||||||
|
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'])]
|
||||||
public ?string $dockerComposeLocation = null;
|
public ?string $dockerComposeLocation = null;
|
||||||
|
|
||||||
#[Validate(['string', 'nullable'])]
|
#[Validate(['string', 'nullable'])]
|
||||||
|
|
|
||||||
|
|
@ -163,10 +163,12 @@ class GithubPrivateRepository extends Component
|
||||||
'selected_repository_owner' => $this->selected_repository_owner,
|
'selected_repository_owner' => $this->selected_repository_owner,
|
||||||
'selected_repository_repo' => $this->selected_repository_repo,
|
'selected_repository_repo' => $this->selected_repository_repo,
|
||||||
'selected_branch_name' => $this->selected_branch_name,
|
'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_owner' => 'required|string|regex:/^[a-zA-Z0-9\-_]+$/',
|
||||||
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
'selected_repository_repo' => 'required|string|regex:/^[a-zA-Z0-9\-_\.]+$/',
|
||||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||||
|
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($validator->fails()) {
|
if ($validator->fails()) {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||||
'is_static' => 'required|boolean',
|
'is_static' => 'required|boolean',
|
||||||
'publish_directory' => 'nullable|string',
|
'publish_directory' => 'nullable|string',
|
||||||
'build_pack' => 'required|string',
|
'build_pack' => 'required|string',
|
||||||
|
'docker_compose_location' => ['nullable', 'string', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function rules()
|
protected function rules()
|
||||||
|
|
@ -75,6 +76,7 @@ class GithubPrivateRepositoryDeployKey extends Component
|
||||||
'is_static' => 'required|boolean',
|
'is_static' => 'required|boolean',
|
||||||
'publish_directory' => 'nullable|string',
|
'publish_directory' => 'nullable|string',
|
||||||
'build_pack' => 'required|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',
|
'publish_directory' => 'nullable|string',
|
||||||
'build_pack' => 'required|string',
|
'build_pack' => 'required|string',
|
||||||
'base_directory' => 'nullable|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()
|
protected function rules()
|
||||||
|
|
@ -82,7 +82,7 @@ class PublicGitRepository extends Component
|
||||||
'publish_directory' => 'nullable|string',
|
'publish_directory' => 'nullable|string',
|
||||||
'build_pack' => 'required|string',
|
'build_pack' => 'required|string',
|
||||||
'base_directory' => 'nullable|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],
|
'git_branch' => ['required', 'string', new ValidGitBranch],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,19 +16,25 @@ class HealthChecks extends Component
|
||||||
#[Validate(['boolean'])]
|
#[Validate(['boolean'])]
|
||||||
public bool $healthCheckEnabled = false;
|
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;
|
public string $healthCheckMethod;
|
||||||
|
|
||||||
#[Validate(['string'])]
|
#[Validate(['required', 'string', 'in:http,https'])]
|
||||||
public string $healthCheckScheme;
|
public string $healthCheckScheme;
|
||||||
|
|
||||||
#[Validate(['string'])]
|
#[Validate(['required', 'string', 'regex:/^[a-zA-Z0-9.\-_]+$/'])]
|
||||||
public string $healthCheckHost;
|
public string $healthCheckHost;
|
||||||
|
|
||||||
#[Validate(['nullable', 'string'])]
|
#[Validate(['nullable', 'integer', 'min:1', 'max:65535'])]
|
||||||
public ?string $healthCheckPort = null;
|
public ?string $healthCheckPort = null;
|
||||||
|
|
||||||
#[Validate(['string'])]
|
#[Validate(['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'])]
|
||||||
public string $healthCheckPath;
|
public string $healthCheckPath;
|
||||||
|
|
||||||
#[Validate(['integer'])]
|
#[Validate(['integer'])]
|
||||||
|
|
@ -54,12 +60,14 @@ class HealthChecks extends Component
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
'healthCheckEnabled' => 'boolean',
|
'healthCheckEnabled' => 'boolean',
|
||||||
'healthCheckPath' => 'string',
|
'healthCheckType' => 'string|in:http,cmd',
|
||||||
'healthCheckPort' => 'nullable|string',
|
'healthCheckCommand' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||||
'healthCheckHost' => 'string',
|
'healthCheckPath' => ['required', 'string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||||
'healthCheckMethod' => 'string',
|
'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',
|
'healthCheckReturnCode' => 'integer',
|
||||||
'healthCheckScheme' => 'string',
|
'healthCheckScheme' => 'required|string|in:http,https',
|
||||||
'healthCheckResponseText' => 'nullable|string',
|
'healthCheckResponseText' => 'nullable|string',
|
||||||
'healthCheckInterval' => 'integer|min:1',
|
'healthCheckInterval' => 'integer|min:1',
|
||||||
'healthCheckTimeout' => 'integer|min:1',
|
'healthCheckTimeout' => 'integer|min:1',
|
||||||
|
|
@ -81,6 +89,8 @@ class HealthChecks extends Component
|
||||||
|
|
||||||
// Sync to model
|
// Sync to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$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_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -98,6 +108,8 @@ class HealthChecks extends Component
|
||||||
} else {
|
} else {
|
||||||
// Sync from model
|
// Sync from model
|
||||||
$this->healthCheckEnabled = $this->resource->health_check_enabled;
|
$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->healthCheckMethod = $this->resource->health_check_method;
|
||||||
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
$this->healthCheckScheme = $this->resource->health_check_scheme;
|
||||||
$this->healthCheckHost = $this->resource->health_check_host;
|
$this->healthCheckHost = $this->resource->health_check_host;
|
||||||
|
|
@ -116,9 +128,12 @@ class HealthChecks extends Component
|
||||||
public function instantSave()
|
public function instantSave()
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$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_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -143,6 +158,8 @@ class HealthChecks extends Component
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$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_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
@ -171,6 +188,8 @@ class HealthChecks extends Component
|
||||||
|
|
||||||
// Sync component properties to model
|
// Sync component properties to model
|
||||||
$this->resource->health_check_enabled = $this->healthCheckEnabled;
|
$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_method = $this->healthCheckMethod;
|
||||||
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
$this->resource->health_check_scheme = $this->healthCheckScheme;
|
||||||
$this->resource->health_check_host = $this->healthCheckHost;
|
$this->resource->health_check_host = $this->healthCheckHost;
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,10 @@ class ResourceOperations extends Component
|
||||||
{
|
{
|
||||||
$this->authorize('update', $this->resource);
|
$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) {
|
if (! $new_destination) {
|
||||||
$new_destination = SwarmDocker::find($destination_id);
|
$new_destination = SwarmDocker::whereHas('server', $teamScope)->find($destination_id);
|
||||||
}
|
}
|
||||||
if (! $new_destination) {
|
if (! $new_destination) {
|
||||||
return $this->addError('destination_id', 'Destination not found.');
|
return $this->addError('destination_id', 'Destination not found.');
|
||||||
|
|
@ -352,7 +353,7 @@ class ResourceOperations extends Component
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->resource);
|
$this->authorize('update', $this->resource);
|
||||||
$new_environment = Environment::findOrFail($environment_id);
|
$new_environment = Environment::ownedByCurrentTeam()->findOrFail($environment_id);
|
||||||
$this->resource->update([
|
$this->resource->update([
|
||||||
'environment_id' => $environment_id,
|
'environment_id' => $environment_id,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,16 @@ class Show extends Component
|
||||||
throw new \Exception('Certificate content cannot be empty.');
|
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.');
|
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) {
|
if ($this->caCertificate) {
|
||||||
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
$this->caCertificate->ssl_certificate = $this->certificateContent;
|
||||||
$this->caCertificate->save();
|
$this->caCertificate->save();
|
||||||
|
|
@ -114,12 +120,14 @@ class Show extends Component
|
||||||
{
|
{
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($this->certificateContent);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@
|
||||||
namespace App\Livewire\Server;
|
namespace App\Livewire\Server;
|
||||||
|
|
||||||
use App\Jobs\DockerCleanupJob;
|
use App\Jobs\DockerCleanupJob;
|
||||||
|
use App\Models\DockerCleanupExecution;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use Cron\CronExpression;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
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\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
|
@ -34,6 +39,53 @@ class DockerCleanup extends Component
|
||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $disableApplicationImageRetention = false;
|
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)
|
public function mount(string $server_uuid)
|
||||||
{
|
{
|
||||||
try {
|
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_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
'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_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' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
'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) {
|
if (isDev() && data_get($this, 'private_key_id') === 0) {
|
||||||
return 'deploy_key';
|
return 'deploy_key';
|
||||||
}
|
}
|
||||||
if (data_get($this, 'private_key_id')) {
|
if (! is_null(data_get($this, 'private_key_id'))) {
|
||||||
return 'deploy_key';
|
return 'deploy_key';
|
||||||
} elseif (data_get($this, 'source')) {
|
} elseif (data_get($this, 'source')) {
|
||||||
return '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
|
public function getLimits(): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,35 @@ namespace App\Models;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
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
|
class ScheduledTask extends BaseModel
|
||||||
{
|
{
|
||||||
use HasSafeStringAttribute;
|
use HasSafeStringAttribute;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||||
|
{
|
||||||
|
return static::where('team_id', $teamId)->orderBy('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,23 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
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
|
class ScheduledTaskExecution extends BaseModel
|
||||||
{
|
{
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
|
||||||
|
|
@ -1452,12 +1452,14 @@ $schema://$host {
|
||||||
$certificateContent = $caCertificate->ssl_certificate;
|
$certificateContent = $caCertificate->ssl_certificate;
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($certificateContent);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class ServiceApplication extends BaseModel
|
||||||
|
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'service.environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function workdir()
|
public function workdir()
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ class ServiceDatabase extends BaseModel
|
||||||
|
|
||||||
public function team()
|
public function team()
|
||||||
{
|
{
|
||||||
return data_get($this, 'environment.project.team');
|
return data_get($this, 'service.environment.project.team');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function workdir()
|
public function workdir()
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||||
$this->getNotificationSettings('discord')?->isEnabled() ||
|
$this->getNotificationSettings('discord')?->isEnabled() ||
|
||||||
$this->getNotificationSettings('slack')?->isEnabled() ||
|
$this->getNotificationSettings('slack')?->isEnabled() ||
|
||||||
$this->getNotificationSettings('telegram')?->isEnabled() ||
|
$this->getNotificationSettings('telegram')?->isEnabled() ||
|
||||||
$this->getNotificationSettings('pushover')?->isEnabled();
|
$this->getNotificationSettings('pushover')?->isEnabled() ||
|
||||||
|
$this->getNotificationSettings('webhook')?->isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function subscriptionEnded()
|
public function subscriptionEnded()
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@ class StandaloneDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function update(User $user, StandaloneDocker $standaloneDocker): bool
|
public function update(User $user, StandaloneDocker $standaloneDocker): bool
|
||||||
{
|
{
|
||||||
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
|
return $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,8 +45,7 @@ class StandaloneDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function delete(User $user, StandaloneDocker $standaloneDocker): bool
|
public function delete(User $user, StandaloneDocker $standaloneDocker): bool
|
||||||
{
|
{
|
||||||
// return $user->isAdmin() && $user->teams->contains('id', $standaloneDocker->server->team_id);
|
return $user->teams->contains('id', $standaloneDocker->server->team_id);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,8 +53,7 @@ class StandaloneDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function restore(User $user, StandaloneDocker $standaloneDocker): bool
|
public function restore(User $user, StandaloneDocker $standaloneDocker): bool
|
||||||
{
|
{
|
||||||
// return false;
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,7 +61,6 @@ class StandaloneDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function forceDelete(User $user, StandaloneDocker $standaloneDocker): bool
|
public function forceDelete(User $user, StandaloneDocker $standaloneDocker): bool
|
||||||
{
|
{
|
||||||
// return false;
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,7 @@ class SwarmDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function update(User $user, SwarmDocker $swarmDocker): bool
|
public function update(User $user, SwarmDocker $swarmDocker): bool
|
||||||
{
|
{
|
||||||
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
|
return $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,8 +45,7 @@ class SwarmDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function delete(User $user, SwarmDocker $swarmDocker): bool
|
public function delete(User $user, SwarmDocker $swarmDocker): bool
|
||||||
{
|
{
|
||||||
// return $user->isAdmin() && $user->teams->contains('id', $swarmDocker->server->team_id);
|
return $user->teams->contains('id', $swarmDocker->server->team_id);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,8 +53,7 @@ class SwarmDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function restore(User $user, SwarmDocker $swarmDocker): bool
|
public function restore(User $user, SwarmDocker $swarmDocker): bool
|
||||||
{
|
{
|
||||||
// return false;
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,7 +61,6 @@ class SwarmDockerPolicy
|
||||||
*/
|
*/
|
||||||
public function forceDelete(User $user, SwarmDocker $swarmDocker): bool
|
public function forceDelete(User $user, SwarmDocker $swarmDocker): bool
|
||||||
{
|
{
|
||||||
// return false;
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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) {
|
if ($this->isRetryableSshError($lastErrorMessage) && $attempt < $maxRetries - 1) {
|
||||||
$delay = $this->calculateRetryDelay($attempt);
|
$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)
|
// Add deployment log if available (for ExecuteRemoteCommand trait)
|
||||||
if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) {
|
if (isset($this->application_deployment_queue) && method_exists($this, 'addRetryLogEntry')) {
|
||||||
$this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage);
|
$this->addRetryLogEntry($attempt + 1, $maxRetries, $delay, $lastErrorMessage);
|
||||||
|
|
@ -133,42 +130,4 @@ trait SshRetryable
|
||||||
|
|
||||||
return null;
|
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',
|
'base_directory' => 'string|nullable',
|
||||||
'publish_directory' => 'string|nullable',
|
'publish_directory' => 'string|nullable',
|
||||||
'health_check_enabled' => 'boolean',
|
'health_check_enabled' => 'boolean',
|
||||||
'health_check_path' => 'string',
|
'health_check_type' => 'string|in:http,cmd',
|
||||||
'health_check_port' => 'string|nullable',
|
'health_check_command' => ['nullable', 'string', 'max:1000', 'regex:/^[a-zA-Z0-9 \-_.\/:=@,+]+$/'],
|
||||||
'health_check_host' => 'string',
|
'health_check_path' => ['string', 'regex:#^[a-zA-Z0-9/\-_.~%]+$#'],
|
||||||
'health_check_method' => 'string',
|
'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_return_code' => 'numeric',
|
||||||
'health_check_scheme' => 'string',
|
'health_check_scheme' => 'string|in:http,https',
|
||||||
'health_check_response_text' => 'string|nullable',
|
'health_check_response_text' => 'string|nullable',
|
||||||
'health_check_interval' => 'numeric',
|
'health_check_interval' => 'numeric',
|
||||||
'health_check_timeout' => 'numeric',
|
'health_check_timeout' => 'numeric',
|
||||||
|
|
@ -132,8 +134,8 @@ function sharedDataApplications()
|
||||||
'manual_webhook_secret_gitlab' => 'string|nullable',
|
'manual_webhook_secret_gitlab' => 'string|nullable',
|
||||||
'manual_webhook_secret_bitbucket' => 'string|nullable',
|
'manual_webhook_secret_bitbucket' => 'string|nullable',
|
||||||
'manual_webhook_secret_gitea' => 'string|nullable',
|
'manual_webhook_secret_gitea' => 'string|nullable',
|
||||||
'dockerfile_location' => 'string|nullable',
|
'dockerfile_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||||
'docker_compose_location' => 'string',
|
'docker_compose_location' => ['string', 'nullable', 'max:255', 'regex:/^\/[a-zA-Z0-9._\-\/]+$/'],
|
||||||
'docker_compose' => 'string|nullable',
|
'docker_compose' => 'string|nullable',
|
||||||
'docker_compose_domains' => 'array|nullable',
|
'docker_compose_domains' => 'array|nullable',
|
||||||
'docker_compose_custom_start_command' => 'string|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;
|
$uuid = $overrides['uuid'] ?? (string) new Cuid2;
|
||||||
$server = $destination->server;
|
$server = $destination->server;
|
||||||
|
|
||||||
|
if ($server->team_id !== currentTeam()->id) {
|
||||||
|
throw new \RuntimeException('Destination does not belong to the current team.');
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare name and URL
|
// Prepare name and URL
|
||||||
$name = $overrides['name'] ?? 'clone-of-'.str($source->name)->limit(20).'-'.$uuid;
|
$name = $overrides['name'] ?? 'clone-of-'.str($source->name)->limit(20).'-'.$uuid;
|
||||||
$applicationSettings = $source->settings;
|
$applicationSettings = $source->settings;
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,9 @@ function checkMinimumDockerEngineVersion($dockerVersion)
|
||||||
}
|
}
|
||||||
function executeInDocker(string $containerId, string $command)
|
function executeInDocker(string $containerId, string $command)
|
||||||
{
|
{
|
||||||
return "docker exec {$containerId} bash -c '{$command}'";
|
$escapedCommand = str_replace("'", "'\\''", $command);
|
||||||
// return "docker exec {$this->deployment_uuid} bash -c '{$command} |& tee -a /proc/1/fd/1; [ \$PIPESTATUS -eq 0 ] || exit \$PIPESTATUS'";
|
|
||||||
|
return "docker exec {$containerId} bash -c '{$escapedCommand}'";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
|
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(
|
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
|
||||||
uuid: $uuid,
|
uuid: $uuid,
|
||||||
domains: $fqdns,
|
domains: $fqdns,
|
||||||
is_force_https_enabled: true,
|
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||||
serviceLabels: $serviceLabels,
|
serviceLabels: $serviceLabels,
|
||||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||||
|
|
@ -1246,7 +1246,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
network: $network,
|
network: $network,
|
||||||
uuid: $uuid,
|
uuid: $uuid,
|
||||||
domains: $fqdns,
|
domains: $fqdns,
|
||||||
is_force_https_enabled: true,
|
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||||
serviceLabels: $serviceLabels,
|
serviceLabels: $serviceLabels,
|
||||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||||
|
|
@ -1260,7 +1260,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
|
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
|
||||||
uuid: $uuid,
|
uuid: $uuid,
|
||||||
domains: $fqdns,
|
domains: $fqdns,
|
||||||
is_force_https_enabled: true,
|
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||||
serviceLabels: $serviceLabels,
|
serviceLabels: $serviceLabels,
|
||||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||||
|
|
@ -1271,7 +1271,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||||
network: $network,
|
network: $network,
|
||||||
uuid: $uuid,
|
uuid: $uuid,
|
||||||
domains: $fqdns,
|
domains: $fqdns,
|
||||||
is_force_https_enabled: true,
|
is_force_https_enabled: $originalResource->isForceHttpsEnabled(),
|
||||||
serviceLabels: $serviceLabels,
|
serviceLabels: $serviceLabels,
|
||||||
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
is_gzip_enabled: $originalResource->isGzipEnabled(),
|
||||||
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,10 @@ function currentTeam()
|
||||||
|
|
||||||
function showBoarding(): bool
|
function showBoarding(): bool
|
||||||
{
|
{
|
||||||
|
if (isDev()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Auth::user()?->isMember()) {
|
if (Auth::user()?->isMember()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ function allowedPathsForUnsubscribedAccounts()
|
||||||
'login',
|
'login',
|
||||||
'logout',
|
'logout',
|
||||||
'force-password-reset',
|
'force-password-reset',
|
||||||
|
'two-factor-challenge',
|
||||||
'livewire/update',
|
'livewire/update',
|
||||||
'admin',
|
'admin',
|
||||||
];
|
];
|
||||||
|
|
@ -95,6 +96,7 @@ function allowedPathsForInvalidAccounts()
|
||||||
'logout',
|
'logout',
|
||||||
'verify',
|
'verify',
|
||||||
'force-password-reset',
|
'force-password-reset',
|
||||||
|
'two-factor-challenge',
|
||||||
'livewire/update',
|
'livewire/update',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
"mockery/mockery": "^1.6.12",
|
"mockery/mockery": "^1.6.12",
|
||||||
"nunomaduro/collision": "^8.8.3",
|
"nunomaduro/collision": "^8.8.3",
|
||||||
"pestphp/pest": "^4.3.2",
|
"pestphp/pest": "^4.3.2",
|
||||||
|
"pestphp/pest-plugin-browser": "^4.2",
|
||||||
"phpstan/phpstan": "^2.1.38",
|
"phpstan/phpstan": "^2.1.38",
|
||||||
"rector/rector": "^2.3.5",
|
"rector/rector": "^2.3.5",
|
||||||
"serversideup/spin": "^3.1.1",
|
"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 [
|
return [
|
||||||
'coolify' => [
|
'coolify' => [
|
||||||
'version' => '4.0.0-beta.463',
|
'version' => '4.0.0-beta.464',
|
||||||
'helper_version' => '1.0.12',
|
'helper_version' => '1.0.12',
|
||||||
'realtime_version' => '1.0.10',
|
'realtime_version' => '1.0.10',
|
||||||
'self_hosted' => env('SELF_HOSTED', true),
|
'self_hosted' => env('SELF_HOSTED', true),
|
||||||
|
|
|
||||||
|
|
@ -54,18 +54,10 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
'testing' => [
|
'testing' => [
|
||||||
'driver' => 'pgsql',
|
'driver' => 'sqlite',
|
||||||
'url' => env('DATABASE_TEST_URL'),
|
'database' => ':memory:',
|
||||||
'host' => env('DB_TEST_HOST', 'postgres'),
|
|
||||||
'port' => env('DB_TEST_PORT', '5432'),
|
|
||||||
'database' => env('DB_TEST_DATABASE', 'coolify_test'),
|
|
||||||
'username' => env('DB_TEST_USERNAME', 'coolify'),
|
|
||||||
'password' => env('DB_TEST_PASSWORD', 'password'),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'prefix' => '',
|
'prefix' => '',
|
||||||
'prefix_indexes' => true,
|
'foreign_key_constraints' => true,
|
||||||
'search_path' => 'public',
|
|
||||||
'sslmode' => 'prefer',
|
|
||||||
],
|
],
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -184,13 +184,13 @@ return [
|
||||||
'connection' => 'redis',
|
'connection' => 'redis',
|
||||||
'balance' => env('HORIZON_BALANCE', 'false'),
|
'balance' => env('HORIZON_BALANCE', 'false'),
|
||||||
'queue' => env('HORIZON_QUEUES', 'high,default'),
|
'queue' => env('HORIZON_QUEUES', 'high,default'),
|
||||||
'maxTime' => 3600,
|
'maxTime' => env('HORIZON_MAX_TIME', 0),
|
||||||
'maxJobs' => 400,
|
'maxJobs' => 400,
|
||||||
'memory' => 128,
|
'memory' => 128,
|
||||||
'tries' => 1,
|
'tries' => 1,
|
||||||
'nice' => 0,
|
'nice' => 0,
|
||||||
'sleep' => 3,
|
'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()
|
public function up()
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE jsonb USING properties::jsonb');
|
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)');
|
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()
|
public function down()
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
DB::statement('DROP INDEX IF EXISTS idx_activity_type_uuid');
|
||||||
DB::statement('ALTER TABLE activity_log ALTER COLUMN properties TYPE json USING properties::json');
|
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
|
public function up(): void
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
foreach ($this->indexes as [$table, $columns, $indexName]) {
|
||||||
if (! $this->indexExists($indexName)) {
|
if (! $this->indexExists($indexName)) {
|
||||||
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
$columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns));
|
||||||
|
|
@ -32,6 +36,10 @@ return new class extends Migration
|
||||||
|
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
|
if (DB::connection()->getDriverName() !== 'pgsql') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->indexes as [, , $indexName]) {
|
foreach ($this->indexes as [, , $indexName]) {
|
||||||
DB::statement("DROP INDEX IF EXISTS \"{$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_repository' => 'coollabsio/coolify-examples',
|
||||||
'git_branch' => 'v4.x',
|
'git_branch' => 'v4.x',
|
||||||
'base_directory' => '/docker-compose',
|
'base_directory' => '/docker-compose',
|
||||||
'docker_compose_location' => 'docker-compose-test.yaml',
|
'docker_compose_location' => '/docker-compose-test.yaml',
|
||||||
'build_pack' => 'dockercompose',
|
'build_pack' => 'dockercompose',
|
||||||
'ports_exposes' => '80',
|
'ports_exposes' => '80',
|
||||||
'environment_id' => 1,
|
'environment_id' => 1,
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ class CaSslCertSeeder extends Seeder
|
||||||
}
|
}
|
||||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||||
|
|
||||||
|
$base64Cert = base64_encode($caCert->ssl_certificate);
|
||||||
|
|
||||||
$commands = collect([
|
$commands = collect([
|
||||||
"mkdir -p $caCertPath",
|
"mkdir -p $caCertPath",
|
||||||
"chown -R 9999:root $caCertPath",
|
"chown -R 9999:root $caCertPath",
|
||||||
"chmod -R 700 $caCertPath",
|
"chmod -R 700 $caCertPath",
|
||||||
"rm -rf $caCertPath/coolify-ca.crt",
|
"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",
|
"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:
|
volumes:
|
||||||
- .:/var/www/html/:cached
|
- .:/var/www/html/:cached
|
||||||
- dev_backups_data:/var/www/html/storage/app/backups
|
- dev_backups_data:/var/www/html/storage/app/backups
|
||||||
|
networks:
|
||||||
|
- coolify
|
||||||
postgres:
|
postgres:
|
||||||
pull_policy: always
|
pull_policy: always
|
||||||
ports:
|
ports:
|
||||||
|
|
@ -76,6 +78,7 @@ services:
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
|
||||||
|
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "curl -fsS http://127.0.0.1:6001/ready && curl -fsS http://127.0.0.1:6002/ready || exit 1" ]
|
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
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ services:
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
|
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
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
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ services:
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
|
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
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
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ RUN apk add --no-cache \
|
||||||
lsof \
|
lsof \
|
||||||
vim
|
vim
|
||||||
|
|
||||||
|
# Install PHP extensions
|
||||||
|
RUN install-php-extensions sockets
|
||||||
|
|
||||||
# Configure shell aliases
|
# Configure shell aliases
|
||||||
RUN echo "alias ll='ls -al'" >> /etc/profile && \
|
RUN echo "alias ll='ls -al'" >> /etc/profile && \
|
||||||
echo "alias a='php artisan'" >> /etc/profile && \
|
echo "alias a='php artisan'" >> /etc/profile && \
|
||||||
|
|
|
||||||
813
openapi.json
813
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": {
|
"\/security\/keys": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
@ -10332,6 +11024,19 @@
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Health check start period in seconds."
|
"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": {
|
"limits_memory": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Memory limit."
|
"description": "Memory limit."
|
||||||
|
|
@ -10771,6 +11476,110 @@
|
||||||
},
|
},
|
||||||
"type": "object"
|
"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": {
|
"Server": {
|
||||||
"description": "Server model",
|
"description": "Server model",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -11283,6 +12092,10 @@
|
||||||
"name": "Resources",
|
"name": "Resources",
|
||||||
"description": "Resources"
|
"description": "Resources"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Scheduled Tasks",
|
||||||
|
"description": "Scheduled Tasks"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Private Keys",
|
"name": "Private Keys",
|
||||||
"description": "Private Keys"
|
"description": "Private Keys"
|
||||||
|
|
|
||||||
565
openapi.yaml
565
openapi.yaml
|
|
@ -5085,6 +5085,478 @@ paths:
|
||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
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:
|
/security/keys:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -6488,6 +6960,16 @@ components:
|
||||||
health_check_start_period:
|
health_check_start_period:
|
||||||
type: integer
|
type: integer
|
||||||
description: 'Health check start period in seconds.'
|
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:
|
limits_memory:
|
||||||
type: string
|
type: string
|
||||||
description: 'Memory limit.'
|
description: 'Memory limit.'
|
||||||
|
|
@ -6807,6 +7289,86 @@ components:
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
Server:
|
||||||
description: 'Server model'
|
description: 'Server model'
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -7161,6 +7723,9 @@ tags:
|
||||||
-
|
-
|
||||||
name: Resources
|
name: Resources
|
||||||
description: Resources
|
description: Resources
|
||||||
|
-
|
||||||
|
name: 'Scheduled Tasks'
|
||||||
|
description: 'Scheduled Tasks'
|
||||||
-
|
-
|
||||||
name: 'Private Keys'
|
name: 'Private Keys'
|
||||||
description: 'Private Keys'
|
description: 'Private Keys'
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ services:
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
|
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
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
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ services:
|
||||||
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID}"
|
||||||
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY}"
|
||||||
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET}"
|
||||||
|
SOKETI_HOST: "${SOKETI_HOST:-0.0.0.0}"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready || exit 1" ]
|
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
|
interval: 5s
|
||||||
|
|
|
||||||
65
package-lock.json
generated
65
package-lock.json
generated
|
|
@ -10,7 +10,8 @@
|
||||||
"@tailwindcss/typography": "0.5.16",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "5.5.0",
|
"@xterm/xterm": "5.5.0",
|
||||||
"ioredis": "5.6.1"
|
"ioredis": "5.6.1",
|
||||||
|
"playwright": "^1.58.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
|
|
@ -949,7 +950,8 @@
|
||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/forms": {
|
"node_modules/@tailwindcss/forms": {
|
||||||
"version": "0.5.10",
|
"version": "0.5.10",
|
||||||
|
|
@ -1402,8 +1404,7 @@
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
|
|
@ -1556,6 +1557,7 @@
|
||||||
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.4.1",
|
"debug": "~4.4.1",
|
||||||
|
|
@ -1570,6 +1572,7 @@
|
||||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2330,7 +2333,6 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -2338,6 +2340,50 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|
@ -2407,7 +2453,6 @@
|
||||||
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tweetnacl": "^1.0.3"
|
"tweetnacl": "^1.0.3"
|
||||||
}
|
}
|
||||||
|
|
@ -2512,6 +2557,7 @@
|
||||||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.4.1"
|
"debug": "~4.4.1"
|
||||||
|
|
@ -2556,8 +2602,7 @@
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|
@ -2609,7 +2654,6 @@
|
||||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -2709,7 +2753,6 @@
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
|
|
@ -2732,6 +2775,7 @@
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2753,6 +2797,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"@tailwindcss/typography": "0.5.16",
|
"@tailwindcss/typography": "0.5.16",
|
||||||
"@xterm/addon-fit": "0.10.0",
|
"@xterm/addon-fit": "0.10.0",
|
||||||
"@xterm/xterm": "5.5.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">
|
<testsuite name="Feature">
|
||||||
<directory suffix="Test.php">./tests/Feature</directory>
|
<directory suffix="Test.php">./tests/Feature</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
<testsuite name="v4">
|
||||||
|
<directory suffix="Test.php">./tests/v4</directory>
|
||||||
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<coverage/>
|
<coverage/>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="array"/>
|
<env name="CACHE_DRIVER" value="array" force="true"/>
|
||||||
<env name="DB_CONNECTION" value="testing"/>
|
<env name="DB_CONNECTION" value="testing" force="true"/>
|
||||||
<env name="DB_TEST_DATABASE" value="coolify"/>
|
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
</php>
|
</php>
|
||||||
<source>
|
<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
|
<?php
|
||||||
function getOldOrLocal($key, $localValue)
|
if (! function_exists('getOldOrLocal')) {
|
||||||
{
|
function getOldOrLocal($key, $localValue)
|
||||||
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
{
|
||||||
|
return old($key) != '' ? old($key) : (app()->environment('local') ? $localValue : '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = getOldOrLocal('name', 'test3 normal user');
|
$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" />
|
<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>
|
</svg>
|
||||||
{{-- Eye-off icon (shown when password is visible) --}}
|
{{-- 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">
|
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
href="{{ route('settings.oauth') }}">
|
href="{{ route('settings.oauth') }}">
|
||||||
OAuth
|
OAuth
|
||||||
</a>
|
</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>
|
<div class="flex-1"></div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,11 @@
|
||||||
<div>
|
<div>
|
||||||
<p class="font-mono font-semibold text-7xl dark:text-warning">419</p>
|
<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>
|
<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
|
<p class="text-base leading-7 dark:text-neutral-300 text-black">Your session has expired. Please log in again to continue.
|
||||||
for.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center mt-10 gap-x-2">
|
<div class="flex items-center mt-10 gap-x-2">
|
||||||
<a href="{{ url()->previous() }}">
|
<a href="/login">
|
||||||
<x-forms.button>Go back</x-forms.button>
|
<x-forms.button>Back to Login</x-forms.button>
|
||||||
</a>
|
|
||||||
<a href="{{ route('dashboard') }}" {{ wireNavigate() }}>
|
|
||||||
<x-forms.button>Dashboard</x-forms.button>
|
|
||||||
</a>
|
</a>
|
||||||
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
<a target="_blank" class="text-xs" href="{{ config('constants.urls.contact') }}">Contact
|
||||||
support
|
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">
|
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 justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div
|
@if (! empty($item['logo']))
|
||||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
<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">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
|
||||||
class="h-5 w-5 text-warning-600 dark:text-warning-400" fill="none"
|
</div>
|
||||||
viewBox="0 0 24 24" stroke="currentColor">
|
@else
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<div
|
||||||
d="M12 4v16m8-8H4" />
|
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||||
</svg>
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
</div>
|
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-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<div class="font-medium text-neutral-900 dark:text-white truncate">
|
<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">
|
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 justify-between gap-3">
|
||||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div
|
<template x-if="item.logo">
|
||||||
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
<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">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg"
|
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
|
||||||
class="h-5 w-5 text-warning-600 dark:text-warning-400"
|
</div>
|
||||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
</template>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<template x-if="!item.logo">
|
||||||
stroke-width="2" d="M12 4v16m8-8H4" />
|
<div
|
||||||
</svg>
|
class="flex-shrink-0 w-10 h-10 rounded-lg bg-warning-100 dark:bg-warning-900/40 flex items-center justify-center">
|
||||||
</div>
|
<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-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-1">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
<div class="font-medium text-neutral-900 dark:text-white truncate"
|
<div class="font-medium text-neutral-900 dark:text-white truncate"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
<nav wire:poll.10000ms="checkStatus" class="pb-6">
|
||||||
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" :title="$lastDeploymentInfo" :lastDeploymentLink="$lastDeploymentLink" />
|
||||||
<div class="navbar-main">
|
<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() }}
|
<a class="{{ request()->routeIs('project.application.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||||
href="{{ route('project.application.configuration', $parameters) }}">
|
href="{{ route('project.application.configuration', $parameters) }}">
|
||||||
Configuration
|
Configuration
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<h1>{{ $title }}</h1>
|
<h1>{{ $title }}</h1>
|
||||||
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
|
||||||
<div class="navbar-main" x-data">
|
<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() }}
|
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" {{ wireNavigate() }}
|
||||||
href="{{ route('project.service.configuration', $parameters) }}">
|
href="{{ route('project.service.configuration', $parameters) }}">
|
||||||
<button>Configuration</button>
|
<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>
|
<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>
|
</x-callout>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{-- Healthcheck Type Selector --}}
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckMethod" label="Method" required>
|
<x-forms.select canGate="update" :canResource="$resource" id="healthCheckType" label="Type" required wire:model.live="healthCheckType">
|
||||||
<option value="GET">GET</option>
|
<option value="http">HTTP</option>
|
||||||
<option value="POST">POST</option>
|
<option value="cmd">CMD</option>
|
||||||
</x-forms.select>
|
</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>
|
</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">
|
<div class="flex gap-2">
|
||||||
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
<x-forms.input canGate="update" :canResource="$resource" min="1" type="number" id="healthCheckInterval" placeholder="30"
|
||||||
label="Interval (s)" required />
|
label="Interval (s)" required />
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,22 @@
|
||||||
<div class="mt-1 mb-6">Configure Docker cleanup settings for your server.</div>
|
<div class="mt-1 mb-6">Configure Docker cleanup settings for your server.</div>
|
||||||
</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 flex-col gap-2">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<h3>Cleanup Configuration</h3>
|
<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\OtherController;
|
||||||
use App\Http\Controllers\Api\ProjectController;
|
use App\Http\Controllers\Api\ProjectController;
|
||||||
use App\Http\Controllers\Api\ResourcesController;
|
use App\Http\Controllers\Api\ResourcesController;
|
||||||
|
use App\Http\Controllers\Api\ScheduledTasksController;
|
||||||
use App\Http\Controllers\Api\SecurityController;
|
use App\Http\Controllers\Api\SecurityController;
|
||||||
use App\Http\Controllers\Api\ServersController;
|
use App\Http\Controllers\Api\ServersController;
|
||||||
use App\Http\Controllers\Api\ServicesController;
|
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::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::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::patch('/projects/{uuid}', [ProjectController::class, 'update_project'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/projects/{uuid}', [ProjectController::class, 'delete_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::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::patch('/servers/{uuid}', [ServersController::class, 'update_server'])->middleware(['api.ability:write']);
|
||||||
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']);
|
Route::delete('/servers/{uuid}', [ServersController::class, 'delete_server'])->middleware(['api.ability:write']);
|
||||||
|
|
||||||
|
|
@ -120,9 +121,9 @@ Route::group([
|
||||||
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
|
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::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}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:write']);
|
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:write']);
|
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::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']);
|
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}', [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::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}/start', [DatabasesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/databases/{uuid}/restart', [DatabasesController::class, 'action_restart'])->middleware(['api.ability:write']);
|
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:write']);
|
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::get('/services', [ServicesController::class, 'services'])->middleware(['api.ability:read']);
|
||||||
Route::post('/services', [ServicesController::class, 'create_service'])->middleware(['api.ability:write']);
|
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::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::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}/start', [ServicesController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
|
||||||
Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware(['api.ability:write']);
|
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:write']);
|
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([
|
Route::group([
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ use App\Livewire\Server\Show as ServerShow;
|
||||||
use App\Livewire\Server\Swarm as ServerSwarm;
|
use App\Livewire\Server\Swarm as ServerSwarm;
|
||||||
use App\Livewire\Settings\Advanced as SettingsAdvanced;
|
use App\Livewire\Settings\Advanced as SettingsAdvanced;
|
||||||
use App\Livewire\Settings\Index as SettingsIndex;
|
use App\Livewire\Settings\Index as SettingsIndex;
|
||||||
|
use App\Livewire\Settings\ScheduledJobs as SettingsScheduledJobs;
|
||||||
use App\Livewire\Settings\Updates as SettingsUpdates;
|
use App\Livewire\Settings\Updates as SettingsUpdates;
|
||||||
use App\Livewire\SettingsBackup;
|
use App\Livewire\SettingsBackup;
|
||||||
use App\Livewire\SettingsEmail;
|
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/backup', SettingsBackup::class)->name('settings.backup');
|
||||||
Route::get('/settings/email', SettingsEmail::class)->name('settings.email');
|
Route::get('/settings/email', SettingsEmail::class)->name('settings.email');
|
||||||
Route::get('/settings/oauth', SettingsOauth::class)->name('settings.oauth');
|
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');
|
Route::get('/profile', ProfileIndex::class)->name('profile');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,26 @@
|
||||||
|
|
||||||
services:
|
services:
|
||||||
beszel-agent:
|
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:
|
environment:
|
||||||
|
# Required
|
||||||
- LISTEN=/beszel_socket/beszel.sock
|
- LISTEN=/beszel_socket/beszel.sock
|
||||||
- HUB_URL=${HUB_URL?}
|
- HUB_URL=$SERVICE_URL_BESZEL
|
||||||
- 'TOKEN=${TOKEN?}'
|
- TOKEN=${TOKEN} # From hub token settings
|
||||||
- 'KEY=${KEY?}'
|
- 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:
|
volumes:
|
||||||
- beszel_agent_data:/var/lib/beszel-agent
|
- beszel_agent_data:/var/lib/beszel-agent
|
||||||
- beszel_socket:/beszel_socket
|
- beszel_socket:/beszel_socket
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
- '/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)
|
# Add the public Key in "Key" env variable and token in the "Token" variable below (These are obtained from Beszel UI)
|
||||||
services:
|
services:
|
||||||
beszel:
|
beszel:
|
||||||
image: 'henrygd/beszel:0.16.1' # Released on 14 Nov 2025
|
image: 'henrygd/beszel:0.18.4' # Released on 21 Feb 2026
|
||||||
environment:
|
environment:
|
||||||
- SERVICE_URL_BESZEL_8090
|
- SERVICE_URL_BESZEL_8090
|
||||||
|
- CONTAINER_DETAILS=${CONTAINER_DETAILS:-true}
|
||||||
|
- SHARE_ALL_SYSTEMS=${SHARE_ALL_SYSTEMS:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- 'beszel_data:/beszel_data'
|
- 'beszel_data:/beszel_data'
|
||||||
- 'beszel_socket:/beszel_socket'
|
- 'beszel_socket:/beszel_socket'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', '/beszel', 'health', '--url', 'http://localhost:8090']
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 10
|
||||||
|
start_period: 5s
|
||||||
beszel-agent:
|
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:
|
environment:
|
||||||
|
# Required
|
||||||
- LISTEN=/beszel_socket/beszel.sock
|
- LISTEN=/beszel_socket/beszel.sock
|
||||||
- HUB_URL=http://beszel:8090
|
- HUB_URL=$SERVICE_URL_BESZEL
|
||||||
- 'TOKEN=${TOKEN}'
|
- TOKEN=${TOKEN} # From hub token settings
|
||||||
- 'KEY=${KEY}'
|
- 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:
|
volumes:
|
||||||
- beszel_agent_data:/var/lib/beszel-agent
|
- beszel_agent_data:/var/lib/beszel-agent
|
||||||
- beszel_socket:/beszel_socket
|
- beszel_socket:/beszel_socket
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock:ro'
|
- '/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
|
# documentation: https://glitchtip.com
|
||||||
# slogan: GlitchTip is a self-hosted, open-source error tracking system.
|
# slogan: GlitchTip is a error tracking system.
|
||||||
# category: monitoring
|
# category: monitoring
|
||||||
# tags: error, tracking, open-source, self-hosted, sentry
|
# tags: error, tracking, sentry
|
||||||
# logo: svgs/glitchtip.png
|
# logo: svgs/glitchtip.png
|
||||||
# port: 8080
|
# port: 8000
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
@ -29,14 +29,14 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
web:
|
web:
|
||||||
image: glitchtip/glitchtip
|
image: glitchtip/glitchtip:6.0
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
- SERVICE_URL_GLITCHTIP_8080
|
- SERVICE_URL_GLITCHTIP_8000
|
||||||
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip}
|
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip}
|
||||||
- SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION
|
- SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION
|
||||||
- EMAIL_URL=${EMAIL_URL:-consolemail://}
|
- EMAIL_URL=${EMAIL_URL:-consolemail://}
|
||||||
|
|
@ -53,7 +53,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
image: glitchtip/glitchtip
|
image: glitchtip/glitchtip:6.0
|
||||||
command: ./bin/run-celery-with-beat.sh
|
command: ./bin/run-celery-with-beat.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
@ -77,7 +77,7 @@ services:
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
image: glitchtip/glitchtip
|
image: glitchtip/glitchtip:6.0
|
||||||
restart: "no"
|
restart: "no"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# ignore: true
|
||||||
# documentation: https://docs.plane.so/self-hosting/methods/docker-compose
|
# documentation: https://docs.plane.so/self-hosting/methods/docker-compose
|
||||||
# slogan: The open source project management tool
|
# slogan: The open source project management tool
|
||||||
# category: productivity
|
# category: productivity
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# ignore: true
|
||||||
# documentation: https://pterodactyl.io/
|
# documentation: https://pterodactyl.io/
|
||||||
# slogan: Pterodactyl is a free, open-source game server management panel
|
# slogan: Pterodactyl is a free, open-source game server management panel
|
||||||
# category: media
|
# category: media
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
# ignore: true
|
||||||
# documentation: https://pterodactyl.io/
|
# documentation: https://pterodactyl.io/
|
||||||
# slogan: Pterodactyl is a free, open-source game server management panel
|
# slogan: Pterodactyl is a free, open-source game server management panel
|
||||||
# category: media
|
# 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