Merge branch 'next' into docker-network-aliases

This commit is contained in:
Piotr Wójcik
2025-03-16 14:50:26 +01:00
committed by GitHub
556 changed files with 74674 additions and 5618 deletions

View File

@@ -1,16 +1,16 @@
# Coolify Configuration
APP_ID= APP_ID=
APP_NAME=Coolify APP_NAME=Coolify
APP_KEY= APP_KEY=
# PostgreSQL Database Configuration
DB_USERNAME=coolify DB_USERNAME=coolify
DB_PASSWORD= DB_PASSWORD=
# Redis Configuration
REDIS_PASSWORD= REDIS_PASSWORD=
# Pusher Configuration
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
ROOT_USERNAME=
ROOT_USER_EMAIL=
ROOT_USER_PASSWORD=

View File

@@ -13,16 +13,16 @@ jobs:
id: stale id: stale
with: with:
stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.' stale-issue-message: 'This issue will be automatically closed in a few days if no response is received. Please provide an update with the requested information.'
stale-pr-message: 'This pull request will be automatically closed in a few days if no response is received. Please update your PR or comment if you would like to continue working on it.' stale-pr-message: 'This pull request requires attention. If no changes or response is received within the next few days, it will be automatically closed. Please update your PR or leave a comment with the requested information.'
close-issue-message: 'This issue has been automatically closed due to inactivity.' close-issue-message: 'This issue has been automatically closed due to inactivity.'
close-pr-message: 'This pull request has been automatically closed due to inactivity.' close-pr-message: 'Thank you for your contribution. Due to inactivity, this PR was automatically closed. If you would like to continue working on this change in the future, feel free to reopen this PR or submit a new one.'
days-before-stale: 14 days-before-stale: 14
days-before-close: 7 days-before-close: 7
stale-issue-label: '⏱︎ Stale' stale-issue-label: '⏱︎ Stale'
stale-pr-label: '⏱︎ Stale' stale-pr-label: '⏱︎ Stale'
only-labels: '💤 Waiting for feedback' only-labels: '💤 Waiting for feedback, 💤 Waiting for changes'
remove-stale-when-updated: true remove-stale-when-updated: true
operations-per-run: 100 operations-per-run: 100
labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback' labels-to-remove-when-unstale: '⏱︎ Stale, 💤 Waiting for feedback, 💤 Waiting for changes'
close-issue-reason: 'not_planned' close-issue-reason: 'not_planned'
exempt-all-milestones: false exempt-all-milestones: false

View File

@@ -19,8 +19,12 @@ jobs:
script: | script: |
const { owner, repo } = context.repo; const { owner, repo } = context.repo;
async function processIssue(issueNumber) { async function processIssue(issueNumber, isFromPR = false, prBaseBranch = null) {
try { try {
if (isFromPR && prBaseBranch !== 'main') {
return;
}
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner, owner,
repo, repo,
@@ -59,19 +63,19 @@ jobs:
} }
} }
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { if (context.eventName === 'issues') {
const issue = context.payload.issue || context.payload.pull_request; await processIssue(context.payload.issue.number);
await processIssue(issue.number);
} }
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') { if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request; const pr = context.payload.pull_request;
if (pr.body) { await processIssue(pr.number);
if (pr.merged && pr.base.ref === 'main' && pr.body) {
const issueReferences = pr.body.match(/#(\d+)/g); const issueReferences = pr.body.match(/#(\d+)/g);
if (issueReferences) { if (issueReferences) {
for (const reference of issueReferences) { for (const reference of issueReferences) {
const issueNumber = parseInt(reference.substring(1)); const issueNumber = parseInt(reference.substring(1));
await processIssue(issueNumber); await processIssue(issueNumber, true, pr.base.ref);
} }
} }
} }

View File

@@ -38,7 +38,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -77,7 +77,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -119,7 +119,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: | run: |

View File

@@ -38,7 +38,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -77,7 +77,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -119,7 +119,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: | run: |

View File

@@ -12,6 +12,7 @@ on:
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/** - templates/**
- CHANGELOG.md
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View File

@@ -42,7 +42,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -82,7 +82,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -125,7 +125,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: | run: |

View File

@@ -42,7 +42,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -82,7 +82,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Build and Push Image - name: Build and Push Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -125,7 +125,7 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
run: | run: |

View File

@@ -12,6 +12,7 @@ on:
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile
- docker/testing-host/Dockerfile - docker/testing-host/Dockerfile
- templates/** - templates/**
- CHANGELOG.md
env: env:
GITHUB_REGISTRY: ghcr.io GITHUB_REGISTRY: ghcr.io

View File

@@ -0,0 +1,36 @@
name: Generate Changelog
on:
push:
branches: [ main ]
workflow_dispatch:
permissions:
contents: write
jobs:
changelog:
name: Generate changelog
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --verbose
env:
OUTPUT: CHANGELOG.md
GITHUB_REPO: ${{ github.repository }}
- name: Commit
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
git add CHANGELOG.md
git commit -m "docs: update changelog"
git push https://${{ secrets.GITHUB_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git main

1
.gitignore vendored
View File

@@ -36,3 +36,4 @@ scripts/load-test/*
.env.dusk.local .env.dusk.local
docker/coolify-realtime/node_modules docker/coolify-realtime/node_modules
.DS_Store .DS_Store
Changelog.md

6844
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@
You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel. You can ask for guidance anytime on our [Discord server](https://coollabs.io/discord) in the `#contribute` channel.
To understand the tech stack, please refer to the [Tech Stack](TECH_STACK.md) document.
## Table of Contents ## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment) 1. [Setup Development Environment](#1-setup-development-environment)

View File

@@ -53,12 +53,13 @@ Special thanks to our biggest sponsors!
* [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. * [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase.
* [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. * [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. * [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management.
* [Convex](https://convex.link/coolify.io) - Convex is the open-source reactive database for web app developers.
* [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. * [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers. * [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers.
* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang. * [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang.
* [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets. * [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets.
* [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. * [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers.
* [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. * [Brand Dev](https://brand.dev/?ref=coolify.io) - The #1 Brand API for B2B software startups - instantly pull logos, fonts, descriptions, social links, slogans, and so much more from any domain via a single api call.
* [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries. * [Jobscollider](https://jobscollider.com/remote-jobs?ref=coolify.io) - A job search platform connecting professionals with remote work opportunities across various industries.
* [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools. * [Hostinger](https://www.hostinger.com/vps/coolify-hosting?ref=coolify.io) - A web hosting provider offering affordable hosting solutions, domain registration, and website building tools.
* [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services. * [Glueops](https://www.glueops.dev/?ref=coolify.io) - A DevOps consulting company providing infrastructure automation and cloud optimization services.
@@ -66,7 +67,7 @@ Special thanks to our biggest sponsors!
* [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses. * [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses.
* [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly. * [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly.
* [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. * [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes.
* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider. * [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - A Fast web hosting provider.
## Github Sponsors ($40+) ## Github Sponsors ($40+)
@@ -75,6 +76,7 @@ Special thanks to our biggest sponsors!
<a href="https://www.runpod.io/?ref=coolify.io"> <a href="https://www.runpod.io/?ref=coolify.io">
<svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a> <svg style="width:60px;height:60px;background:#fff;" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 200"><g><path d="M74.5 51.1c-25.4 14.9-27 16-29.6 20.2-1.8 3-1.9 5.3-1.9 32.3 0 21.7.3 29.4 1.3 30.6 1.9 2.5 46.7 27.9 48.5 27.6 1.5-.3 1.7-3.1 2-27.7.2-21.9 0-27.8-1.1-29.5-.8-1.2-9.9-6.8-20.2-12.6-10.3-5.8-19.4-11.5-20.2-12.7-1.8-2.6-.9-5.9 1.8-7.4 1.6-.8 6.3 0 21.8 4C87.8 78.7 98 81 99.6 81c4.4 0 49.9-25.9 49.9-28.4 0-1.6-3.4-2.8-24-8.2-13.2-3.5-25.1-6.3-26.5-6.3-1.4.1-12.4 5.9-24.5 13z"></path><path d="m137.2 68.1-3.3 2.1 6.3 3.7c3.5 2 6.3 4.3 6.3 5.1 0 .9-8 6.1-19.4 12.6-10.6 6-20 11.9-20.7 12.9-1.2 1.6-1.4 7.2-1.2 29.4.3 24.8.5 27.6 2 27.9 1.8.3 46.6-25.1 48.6-27.6.9-1.2 1.2-8.8 1.2-30.2s-.3-29-1.2-30.2c-1.6-1.9-12.1-7.8-13.9-7.8-.8 0-2.9 1-4.7 2.1z"></path></g></svg></a>
<a href="https://lightspeed.run/?ref=coolify.io"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a> <a href="https://lightspeed.run/?ref=coolify.io"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
<a href="https://dartnode.com/?ref=coolify.io"><img src="https://github.com/DartNode-com.png" width="60px" alt="DartNode"/></a>
<a href="https://www.flint.sh/en/home?ref=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a> <a href="https://www.flint.sh/en/home?ref=coolify.io"> <img src="https://github.com/Flint-company.png" width="60px" alt="FlintCompany"/></a>
<a href="https://americancloud.com/?ref=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a> <a href="https://americancloud.com/?ref=coolify.io"><img src="https://github.com/American-Cloud.png" width="60px" alt="American Cloud"/></a>
<a href="https://cryptojobslist.com/?ref=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a> <a href="https://cryptojobslist.com/?ref=coolify.io"><img src="https://github.com/cryptojobslist.png" width="60px" alt="CryptoJobsList" /></a>

29
TECH_STACK.md Normal file
View File

@@ -0,0 +1,29 @@
# Coolify Technology Stack
## Frontend
- Livewire and Alpine.js
- Blade (PHP templating engine)
- Tailwind CSS
- Monaco Editor (Code editor component)
- XTerm.js (Terminal component)
## Backend
- Laravel 11 (PHP Framework)
- PostgreSQL 15 (Database)
- Redis 7 (Caching & Real-time features)
- Soketi (WebSocket Server)
## DevOps & Infrastructure
- Docker & Docker Compose
- Nginx (Web Server)
- S6 Overlay (Process Supervisor)
- GitHub Actions (CI/CD)
## Languages
- PHP 8.4
- JavaScript
- Shell/Bash scripts

View File

@@ -91,16 +91,9 @@ class RunRemoteProcess
} else { } else {
if ($processResult->exitCode() == 0) { if ($processResult->exitCode() == 0) {
$status = ProcessStatus::FINISHED; $status = ProcessStatus::FINISHED;
} } else {
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
$status = ProcessStatus::ERROR; $status = ProcessStatus::ERROR;
} }
// if (($processResult->exitCode() == 0 && $this->is_finished) || $this->activity->properties->get('status') === ProcessStatus::FINISHED->value) {
// $status = ProcessStatus::FINISHED;
// }
// if ($processResult->exitCode() != 0 && !$this->ignore_errors) {
// $status = ProcessStatus::ERROR;
// }
} }
$this->activity->properties = $this->activity->properties->merge([ $this->activity->properties = $this->activity->properties->merge([
@@ -110,9 +103,6 @@ class RunRemoteProcess
'status' => $status->value, 'status' => $status->value,
]); ]);
$this->activity->save(); $this->activity->save();
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
}
if ($this->call_event_on_finish) { if ($this->call_event_on_finish) {
try { try {
if ($this->call_event_data) { if ($this->call_event_data) {
@@ -128,6 +118,9 @@ class RunRemoteProcess
Log::error('Error calling event: '.$e->getMessage()); Log::error('Error calling event: '.$e->getMessage());
} }
} }
if ($processResult->exitCode() != 0 && ! $this->ignore_errors) {
throw new \RuntimeException($processResult->errorOutput(), $processResult->exitCode());
}
return $processResult; return $processResult;
} }

View File

@@ -49,11 +49,7 @@ class StartClickhouse
'hard' => 262144, 'hard' => 262144,
], ],
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", 'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'",
'interval' => '5s', 'interval' => '5s',

View File

@@ -22,70 +22,27 @@ class StartDatabaseProxy
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database) public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse|ServiceDatabase $database)
{ {
$internalPort = null; $databaseType = $database->database_type;
$type = $database->getMorphClass();
$network = data_get($database, 'destination.network'); $network = data_get($database, 'destination.network');
$server = data_get($database, 'destination.server'); $server = data_get($database, 'destination.server');
$containerName = data_get($database, 'uuid'); $containerName = data_get($database, 'uuid');
$proxyContainerName = "{$database->uuid}-proxy"; $proxyContainerName = "{$database->uuid}-proxy";
if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$databaseType = $database->databaseType(); $databaseType = $database->databaseType();
// $connectPredefined = data_get($database, 'service.connect_to_docker_network');
$network = $database->service->uuid; $network = $database->service->uuid;
$server = data_get($database, 'service.destination.server'); $server = data_get($database, 'service.destination.server');
$proxyContainerName = "{$database->service->uuid}-proxy"; $proxyContainerName = "{$database->service->uuid}-proxy";
switch ($databaseType) { $containerName = "{$database->name}-{$database->service->uuid}";
case 'standalone-mariadb':
$type = \App\Models\StandaloneMariadb::class;
$containerName = "mariadb-{$database->service->uuid}";
break;
case 'standalone-mongodb':
$type = \App\Models\StandaloneMongodb::class;
$containerName = "mongodb-{$database->service->uuid}";
break;
case 'standalone-mysql':
$type = \App\Models\StandaloneMysql::class;
$containerName = "mysql-{$database->service->uuid}";
break;
case 'standalone-postgresql':
$type = \App\Models\StandalonePostgresql::class;
$containerName = "postgresql-{$database->service->uuid}";
break;
case 'standalone-redis':
$type = \App\Models\StandaloneRedis::class;
$containerName = "redis-{$database->service->uuid}";
break;
case 'standalone-keydb':
$type = \App\Models\StandaloneKeydb::class;
$containerName = "keydb-{$database->service->uuid}";
break;
case 'standalone-dragonfly':
$type = \App\Models\StandaloneDragonfly::class;
$containerName = "dragonfly-{$database->service->uuid}";
break;
case 'standalone-clickhouse':
$type = \App\Models\StandaloneClickhouse::class;
$containerName = "clickhouse-{$database->service->uuid}";
break;
}
}
if ($type === \App\Models\StandaloneRedis::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandalonePostgresql::class) {
$internalPort = 5432;
} elseif ($type === \App\Models\StandaloneMongodb::class) {
$internalPort = 27017;
} elseif ($type === \App\Models\StandaloneMysql::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneMariadb::class) {
$internalPort = 3306;
} elseif ($type === \App\Models\StandaloneKeydb::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneDragonfly::class) {
$internalPort = 6379;
} elseif ($type === \App\Models\StandaloneClickhouse::class) {
$internalPort = 9000;
} }
$internalPort = match ($databaseType) {
'standalone-mariadb', 'standalone-mysql' => 3306,
'standalone-postgresql', 'standalone-supabase/postgres' => 5432,
'standalone-redis', 'standalone-keydb', 'standalone-dragonfly' => 6379,
'standalone-clickhouse' => 9000,
'standalone-mongodb' => 27017,
default => throw new \Exception("Unsupported database type: $databaseType"),
};
$configuration_dir = database_proxy_dir($database->uuid); $configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF $nginxconf = <<<EOF
user nginx; user nginx;

View File

@@ -46,11 +46,7 @@ class StartDragonfly
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => "redis-cli -a {$this->database->dragonfly_password} ping", 'test' => "redis-cli -a {$this->database->dragonfly_password} ping",
'interval' => '5s', 'interval' => '5s',

View File

@@ -48,11 +48,7 @@ class StartKeydb
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => "keydb-cli --pass {$this->database->keydb_password} ping", 'test' => "keydb-cli --pass {$this->database->keydb_password} ping",
'interval' => '5s', 'interval' => '5s',

View File

@@ -43,11 +43,7 @@ class StartMariadb
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'], 'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
'interval' => '5s', 'interval' => '5s',

View File

@@ -51,11 +51,7 @@ class StartMongodb
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD', 'CMD',

View File

@@ -43,11 +43,7 @@ class StartMysql
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"], 'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
'interval' => '5s', 'interval' => '5s',

View File

@@ -23,6 +23,9 @@ class StartPostgresql
$this->database = $database; $this->database = $database;
$container_name = $this->database->uuid; $container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir().'/'.$container_name; $this->configuration_dir = database_configuration_dir().'/'.$container_name;
if (isDev()) {
$this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name;
}
$this->commands = [ $this->commands = [
"echo 'Starting database.'", "echo 'Starting database.'",
@@ -47,11 +50,7 @@ class StartPostgresql
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD-SHELL', 'CMD-SHELL',
@@ -78,7 +77,7 @@ class StartPostgresql
], ],
], ],
]; ];
if (! is_null($this->database->limits_cpuset)) { if (filled($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
} }
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -108,7 +107,7 @@ class StartPostgresql
]; ];
} }
} }
if (! is_null($this->database->postgres_conf) && ! empty($this->database->postgres_conf)) { if (filled($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir.'/custom-postgres.conf', 'source' => $this->configuration_dir.'/custom-postgres.conf',
@@ -199,9 +198,12 @@ class StartPostgresql
private function generate_init_scripts() private function generate_init_scripts()
{ {
if (is_null($this->database->init_scripts) || count($this->database->init_scripts) === 0) { $this->commands[] = "rm -rf $this->configuration_dir/docker-entrypoint-initdb.d/*";
if (blank($this->database->init_scripts) || count($this->database->init_scripts) === 0) {
return; return;
} }
foreach ($this->database->init_scripts as $init_script) { foreach ($this->database->init_scripts as $init_script) {
$filename = data_get($init_script, 'filename'); $filename = data_get($init_script, 'filename');
$content = data_get($init_script, 'content'); $content = data_get($init_script, 'content');
@@ -213,10 +215,15 @@ class StartPostgresql
private function add_custom_conf() private function add_custom_conf()
{ {
if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) { $filename = 'custom-postgres.conf';
$config_file_path = "$this->configuration_dir/$filename";
if (blank($this->database->postgres_conf)) {
$this->commands[] = "rm -f $config_file_path";
return; return;
} }
$filename = 'custom-postgres.conf';
$content = $this->database->postgres_conf; $content = $this->database->postgres_conf;
if (! str($content)->contains('listen_addresses')) { if (! str($content)->contains('listen_addresses')) {
$content .= "\nlisten_addresses = '*'"; $content .= "\nlisten_addresses = '*'";
@@ -224,6 +231,6 @@ class StartPostgresql
$this->database->save(); $this->database->save();
} }
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $config_file_path > /dev/null";
} }
} }

View File

@@ -48,11 +48,7 @@ class StartRedis
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'labels' => [ 'labels' => defaultDatabaseLabels($this->database)->toArray(),
'coolify.managed' => 'true',
'coolify.type' => 'database',
'coolify.databaseId' => $this->database->id,
],
'healthcheck' => [ 'healthcheck' => [
'test' => [ 'test' => [
'CMD-SHELL', 'CMD-SHELL',

View File

@@ -30,7 +30,6 @@ class StopDatabaseProxy
} }
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
$database->is_public = false;
$database->save(); $database->save();
DatabaseProxyStopped::dispatch(); DatabaseProxyStopped::dispatch();

View File

@@ -208,7 +208,6 @@ class GetContainersStatus
$foundServices[] = "$service->id-$service->name"; $foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status; $statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) { if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]); $service->update(['status' => $containerStatus]);
} else { } else {
$service->update(['last_online_at' => now()]); $service->update(['last_online_at' => now()]);

View File

@@ -28,13 +28,13 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save(); $server->save();
if ($server->isSwarm()) { if ($server->isSwarmManager()) {
$commands = $commands->merge([ $commands = $commands->merge([
"mkdir -p $proxy_path/dynamic", "mkdir -p $proxy_path/dynamic",
"cd $proxy_path", "cd $proxy_path",
"echo 'Creating required Docker Compose file.'", "echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
'docker stack deploy -c docker-compose.yml coolify-proxy', 'docker stack deploy --detach=true -c docker-compose.yml coolify-proxy',
"echo 'Successfully started coolify-proxy.'", "echo 'Successfully started coolify-proxy.'",
]); ]);
} else { } else {
@@ -57,7 +57,7 @@ class StartProxy
" echo 'Successfully stopped and removed existing coolify-proxy.'", " echo 'Successfully stopped and removed existing coolify-proxy.'",
'fi', 'fi',
"echo 'Starting coolify-proxy.'", "echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans', 'docker compose up -d',
"echo 'Successfully started coolify-proxy.'", "echo 'Successfully started coolify-proxy.'",
]); ]);
$commands = $commands->merge(connectProxyToNetworks($server)); $commands = $commands->merge(connectProxyToNetworks($server));

View File

@@ -25,17 +25,25 @@ class CleanupDocker
"docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f", "docker images --filter before=$helperImageWithVersion --filter reference=$helperImage | grep $helperImage | awk '{print $3}' | xargs -r docker rmi -f",
]; ];
$serverSettings = $server->settings; if ($server->settings->delete_unused_volumes) {
if ($serverSettings->delete_unused_volumes) {
$commands[] = 'docker volume prune -af'; $commands[] = 'docker volume prune -af';
} }
if ($serverSettings->delete_unused_networks) { if ($server->settings->delete_unused_networks) {
$commands[] = 'docker network prune -f'; $commands[] = 'docker network prune -f';
} }
$cleanupLog = [];
foreach ($commands as $command) { foreach ($commands as $command) {
instant_remote_process([$command], $server, false); $commandOutput = instant_remote_process([$command], $server, false);
if ($commandOutput !== null) {
$cleanupLog[] = [
'command' => $command,
'output' => $commandOutput,
];
} }
} }
return $cleanupLog;
}
} }

View File

@@ -12,19 +12,24 @@ class StartService
public string $jobQueue = 'high'; public string $jobQueue = 'high';
public function handle(Service $service) public function handle(Service $service, bool $pullLatestImages = false, bool $stopBeforeStart = false)
{ {
$service->parse();
if ($stopBeforeStart) {
StopService::run(service: $service, dockerCleanup: false);
}
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir(); $commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
if ($pullLatestImages) {
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';
}
if ($service->networks()->count() > 0) { if ($service->networks()->count() > 0) {
$commands[] = "echo 'Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid"; $commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
} }
$commands[] = 'echo Starting service.'; $commands[] = 'echo Starting service.';
$commands[] = "echo 'Pulling images.'";
$commands[] = 'docker compose pull';
$commands[] = "echo 'Starting containers.'";
$commands[] = 'docker compose up -d --remove-orphans --force-recreate --build'; $commands[] = 'docker compose up -d --remove-orphans --force-recreate --build';
$commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true"; $commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
if (data_get($service, 'connect_to_docker_network')) { if (data_get($service, 'connect_to_docker_network')) {

View File

@@ -1,28 +0,0 @@
<?php
namespace App\Actions\Shared;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class PullImage
{
use AsAction;
public function handle(Service $resource)
{
$resource->saveComposeConfigs();
$commands[] = 'cd '.$resource->workdir();
$commands[] = "echo 'Saved configuration files to {$resource->workdir()}.'";
$commands[] = 'docker compose pull';
$server = data_get($resource, 'server');
if (! $server) {
return;
}
instant_remote_process($commands, $resource->server);
}
}

View File

@@ -57,6 +57,14 @@ class CleanupDatabase extends Command
$application_deployment_queues->delete(); $application_deployment_queues->delete();
} }
// Cleanup scheduled_task_executions table
$scheduled_task_executions = DB::table('scheduled_task_executions')->where('created_at', '<', now()->subDays($keep_days))->orderBy('created_at', 'desc');
$count = $scheduled_task_executions->count();
echo "Delete $count entries from scheduled_task_executions.\n";
if ($this->option('yes')) {
$scheduled_task_executions->delete();
}
// Cleanup webhooks table // Cleanup webhooks table
$webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days)); $webhooks = DB::table('webhooks')->where('created_at', '<', now()->subDays($keep_days));
$count = $webhooks->count(); $count = $webhooks->count();

View File

@@ -39,6 +39,11 @@ class CleanupStuckedResources extends Command
$servers = Server::all()->filter(function ($server) { $servers = Server::all()->filter(function ($server) {
return $server->isFunctional(); return $server->isFunctional();
}); });
if (isCloud()) {
$servers = $servers->filter(function ($server) {
return data_get($server->team->subscription, 'stripe_invoice_paid', false) === true;
});
}
foreach ($servers as $server) { foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server); CleanupHelperContainersJob::dispatch($server);
} }

View File

@@ -50,7 +50,7 @@ class CloudCleanupSubscriptions extends Command
} else { } else {
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []); $subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
$status = data_get($subscription, 'status'); $status = data_get($subscription, 'status');
if ($status === 'active' || $status === 'past_due') { if ($status === 'active') {
$team->subscription->update([ $team->subscription->update([
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_trial_already_ended' => false, 'stripe_trial_already_ended' => false,

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use App\Repositories\CustomJobRepository;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Repositories\RedisJobRepository;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
use function Laravel\Prompts\table;
use function Laravel\Prompts\text;
class HorizonManage extends Command
{
protected $signature = 'horizon:manage {--can-i-restart-this-worker} {--job-status=}';
protected $description = 'Manage horizon';
public function handle()
{
if ($this->option('can-i-restart-this-worker')) {
return $this->isThereAJobInProgress();
}
if ($this->option('job-status')) {
return $this->getJobStatus($this->option('job-status'));
}
$action = select(
label: 'What to do?',
options: [
'pending' => 'Pending Jobs',
'running' => 'Running Jobs',
'can-i-restart-this-worker' => 'Can I restart this worker?',
'job-status' => 'Job Status',
'workers' => 'Workers',
'failed' => 'Failed Jobs',
'failed-delete' => 'Failed Jobs - Delete',
'purge-queues' => 'Purge Queues',
]
);
if ($action === 'can-i-restart-this-worker') {
$this->isThereAJobInProgress();
}
if ($action === 'job-status') {
$jobId = text('Which job to check?');
$jobStatus = $this->getJobStatus($jobId);
$this->info('Job Status: '.$jobStatus);
}
if ($action === 'pending') {
$pendingJobs = app(JobRepository::class)->getPending();
$pendingJobsTable = [];
if (count($pendingJobs) === 0) {
$this->info('No pending jobs found.');
return;
}
foreach ($pendingJobs as $pendingJob) {
$pendingJobsTable[] = [
'id' => $pendingJob->id,
'name' => $pendingJob->name,
'status' => $pendingJob->status,
'reserved_at' => $pendingJob->reserved_at ? now()->parse($pendingJob->reserved_at)->format('Y-m-d H:i:s') : null,
];
}
table($pendingJobsTable);
}
if ($action === 'failed') {
$failedJobs = app(JobRepository::class)->getFailed();
$failedJobsTable = [];
if (count($failedJobs) === 0) {
$this->info('No failed jobs found.');
return;
}
foreach ($failedJobs as $failedJob) {
$failedJobsTable[] = [
'id' => $failedJob->id,
'name' => $failedJob->name,
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
];
}
table($failedJobsTable);
}
if ($action === 'failed-delete') {
$failedJobs = app(JobRepository::class)->getFailed();
$failedJobsTable = [];
foreach ($failedJobs as $failedJob) {
$failedJobsTable[] = [
'id' => $failedJob->id,
'name' => $failedJob->name,
'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null,
];
}
app(MetricsRepository::class)->clear();
if (count($failedJobsTable) === 0) {
$this->info('No failed jobs found.');
return;
}
$jobIds = multiselect(
label: 'Which job to delete?',
options: collect($failedJobsTable)->mapWithKeys(fn ($job) => [$job['id'] => $job['id'].' - '.$job['name']])->toArray(),
);
foreach ($jobIds as $jobId) {
Artisan::queue('horizon:forget', ['id' => $jobId]);
}
}
if ($action === 'running') {
$redisJobRepository = app(CustomJobRepository::class);
$runningJobs = $redisJobRepository->getReservedJobs();
$runningJobsTable = [];
if (count($runningJobs) === 0) {
$this->info('No running jobs found.');
return;
}
foreach ($runningJobs as $runningJob) {
$runningJobsTable[] = [
'id' => $runningJob->id,
'name' => $runningJob->name,
'reserved_at' => $runningJob->reserved_at ? now()->parse($runningJob->reserved_at)->format('Y-m-d H:i:s') : null,
];
}
table($runningJobsTable);
}
if ($action === 'workers') {
$redisJobRepository = app(CustomJobRepository::class);
$workers = $redisJobRepository->getHorizonWorkers();
$workersTable = [];
foreach ($workers as $worker) {
$workersTable[] = [
'name' => $worker->name,
];
}
table($workersTable);
}
if ($action === 'purge-queues') {
$getQueues = app(CustomJobRepository::class)->getQueues();
$queueName = select(
label: 'Which queue to purge?',
options: $getQueues,
);
$redisJobRepository = app(RedisJobRepository::class);
$redisJobRepository->purge($queueName);
}
}
public function isThereAJobInProgress()
{
$runningJobs = ApplicationDeploymentQueue::where('horizon_job_worker', gethostname())->where('status', ApplicationDeploymentStatus::IN_PROGRESS->value)->get();
$count = $runningJobs->count();
if ($count === 0) {
return false;
}
return true;
}
public function getJobStatus(string $jobId)
{
return getJobStatus($jobId);
}
}

View File

@@ -35,8 +35,7 @@ class Init extends Command
} }
$this->servers = Server::all(); $this->servers = Server::all();
if (isCloud()) { if (! isCloud()) {
} else {
$this->send_alive_signal(); $this->send_alive_signal();
get_public_ips(); get_public_ips();
} }
@@ -88,8 +87,10 @@ class Init extends Command
$settings = instanceSettings(); $settings = instanceSettings();
if (! is_null(config('constants.coolify.autoupdate', null))) { if (! is_null(config('constants.coolify.autoupdate', null))) {
if (config('constants.coolify.autoupdate') == true) { if (config('constants.coolify.autoupdate') == true) {
echo "Enabling auto-update\n";
$settings->update(['is_auto_update_enabled' => true]); $settings->update(['is_auto_update_enabled' => true]);
} else { } else {
echo "Disabling auto-update\n";
$settings->update(['is_auto_update_enabled' => false]); $settings->update(['is_auto_update_enabled' => false]);
} }
} }
@@ -119,7 +120,9 @@ class Init extends Command
private function update_user_emails() private function update_user_emails()
{ {
try { try {
User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)])); User::whereRaw('email ~ \'[A-Z]\'')->get()->each(function (User $user) {
$user->update(['email' => strtolower($user->email)]);
});
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in updating user emails: {$e->getMessage()}\n"; echo "Error in updating user emails: {$e->getMessage()}\n";
} }
@@ -200,7 +203,6 @@ class Init extends Command
try { try {
$database = StandalonePostgresql::withTrashed()->find(0); $database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) { if ($database && $database->trashed()) {
echo "Restoring coolify db backup\n";
$database->restore(); $database->restore();
$scheduledBackup = ScheduledDatabaseBackup::find(0); $scheduledBackup = ScheduledDatabaseBackup::find(0);
if (! $scheduledBackup) { if (! $scheduledBackup) {

View File

@@ -6,13 +6,11 @@ use App\Jobs\CheckAndStartSentinelJob;
use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CheckHelperImageJob; use App\Jobs\CheckHelperImageJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\CleanupStaleMultiplexedConnections;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\PullTemplatesFromCDN; use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob; use App\Jobs\ServerCheckJob;
use App\Jobs\ServerCleanupMux;
use App\Jobs\ServerStorageCheckJob; use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob; use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
@@ -23,6 +21,7 @@ use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
@@ -91,13 +90,23 @@ class Kernel extends ConsoleKernel
private function pullImages(): void private function pullImages(): void
{ {
if (isCloud()) {
$servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else {
$servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get();
}
foreach ($servers as $server) { foreach ($servers as $server) {
try {
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {
$this->scheduleInstance->job(function () use ($server) { $this->scheduleInstance->job(function () use ($server) {
CheckAndStartSentinelJob::dispatch($server); CheckAndStartSentinelJob::dispatch($server);
})->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer();
} }
} catch (\Exception $e) {
Log::error('Error pulling images: '.$e->getMessage());
}
} }
$this->scheduleInstance->job(new CheckHelperImageJob) $this->scheduleInstance->job(new CheckHelperImageJob)
->cron($this->updateCheckFrequency) ->cron($this->updateCheckFrequency)
@@ -124,7 +133,7 @@ class Kernel extends ConsoleKernel
private function checkResources(): void private function checkResources(): void
{ {
if (isCloud()) { if (isCloud()) {
$servers = $this->allServers->whereHas('team.subscription')->get(); $servers = $this->allServers->whereRelation('team.subscription', 'stripe_invoice_paid', true)->get();
$own = Team::find(0)->servers; $own = Team::find(0)->servers;
$servers = $servers->merge($own); $servers = $servers->merge($own);
} else { } else {
@@ -132,15 +141,16 @@ class Kernel extends ConsoleKernel
} }
foreach ($servers as $server) { foreach ($servers as $server) {
try {
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
// Sentinel check // Sentinel check
$lastSentinelUpdate = $server->sentinel_updated_at; $lastSentinelUpdate = $server->sentinel_updated_at;
if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) {
// Check container status every minute if Sentinel does not activated // Check container status every minute if Sentinel does not activated
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
if (isCloud()) { if (isCloud()) {
$this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer();
} else { } else {
@@ -148,15 +158,19 @@ class Kernel extends ConsoleKernel
} }
// $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer();
// Check storage usage every 10 minutes if Sentinel does not activated $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *');
$this->scheduleInstance->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) {
$serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency];
} }
if ($server->settings->force_docker_cleanup) { $this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer();
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();
} else {
$this->scheduleInstance->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer();
} }
$dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *');
if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) {
$dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency];
}
$this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer();
// Cleanup multiplexed connections every hour // Cleanup multiplexed connections every hour
// $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
@@ -166,6 +180,9 @@ class Kernel extends ConsoleKernel
$server->restartContainer('coolify-sentinel'); $server->restartContainer('coolify-sentinel');
})->daily()->onOneServer(); })->daily()->onOneServer();
} }
} catch (\Exception $e) {
Log::error('Error checking resources: '.$e->getMessage());
}
} }
} }
@@ -175,25 +192,51 @@ class Kernel extends ConsoleKernel
if ($scheduled_backups->isEmpty()) { if ($scheduled_backups->isEmpty()) {
return; return;
} }
$finalScheduledBackups = collect();
foreach ($scheduled_backups as $scheduled_backup) { foreach ($scheduled_backups as $scheduled_backup) {
if (is_null(data_get($scheduled_backup, 'database'))) { if (blank(data_get($scheduled_backup, 'database'))) {
$scheduled_backup->delete(); $scheduled_backup->delete();
continue; continue;
} }
$server = $scheduled_backup->server(); $server = $scheduled_backup->server();
if (blank($server)) {
$scheduled_backup->delete();
if (is_null($server)) {
continue; continue;
} }
if ($server->isFunctional() === false) {
continue;
}
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
continue;
}
$finalScheduledBackups->push($scheduled_backup);
}
foreach ($finalScheduledBackups as $scheduled_backup) {
try {
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
}
$server = $scheduled_backup->server();
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) {
$scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency];
} }
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
$this->scheduleInstance->job(new DatabaseBackupJob( $this->scheduleInstance->job(new DatabaseBackupJob(
backup: $scheduled_backup backup: $scheduled_backup
))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer(); ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer();
} catch (\Exception $e) {
Log::error('Error scheduling backup: '.$e->getMessage());
Log::error($e->getTraceAsString());
}
} }
} }
@@ -203,37 +246,60 @@ class Kernel extends ConsoleKernel
if ($scheduled_tasks->isEmpty()) { if ($scheduled_tasks->isEmpty()) {
return; return;
} }
$finalScheduledTasks = collect();
foreach ($scheduled_tasks as $scheduled_task) { foreach ($scheduled_tasks as $scheduled_task) {
$service = $scheduled_task->service; $service = $scheduled_task->service;
$application = $scheduled_task->application; $application = $scheduled_task->application;
if (! $application && ! $service) { $server = $scheduled_task->server();
if (blank($server)) {
$scheduled_task->delete(); $scheduled_task->delete();
continue; continue;
} }
if ($application) {
if (str($application->status)->contains('running') === false) { if ($server->isFunctional() === false) {
continue; continue;
} }
}
if ($service) {
if (str($service->status)->contains('running') === false) {
continue;
}
}
if (isCloud() && data_get($server->team->subscription, 'stripe_invoice_paid', false) === false && $server->team->id !== 0) {
continue;
}
if (! $service && ! $application) {
$scheduled_task->delete();
continue;
}
if ($application && str($application->status)->contains('running') === false) {
continue;
}
if ($service && str($service->status)->contains('running') === false) {
continue;
}
$finalScheduledTasks->push($scheduled_task);
}
foreach ($finalScheduledTasks as $scheduled_task) {
try {
$server = $scheduled_task->server(); $server = $scheduled_task->server();
if (! $server) {
continue;
}
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
} }
$serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone);
if (validate_timezone($serverTimezone) === false) {
$serverTimezone = config('app.timezone');
}
$this->scheduleInstance->job(new ScheduledTaskJob( $this->scheduleInstance->job(new ScheduledTaskJob(
task: $scheduled_task task: $scheduled_task
))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer(); ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer();
} catch (\Exception $e) {
Log::error('Error scheduling task: '.$e->getMessage());
Log::error($e->getTraceAsString());
}
} }
} }

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Contracts;
use Illuminate\Support\Collection;
use Laravel\Horizon\Contracts\JobRepository;
interface CustomJobRepositoryInterface extends JobRepository
{
/**
* Get all jobs with a specific status.
*/
public function getJobsByStatus(string $status): Collection;
/**
* Get the count of jobs with a specific status.
*/
public function countJobsByStatus(string $status): int;
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Events;
use App\Models\DockerCleanupExecution;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DockerCleanupDone implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public DockerCleanupExecution $execution) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('team.'.$this->execution->server->team->id),
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use App\Models\Server;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class RestoreJobFinished
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct($data)
{
$scriptPath = data_get($data, 'scriptPath');
$tmpPath = data_get($data, 'tmpPath');
$container = data_get($data, 'container');
$serverId = data_get($data, 'serverId');
if (filled($scriptPath) && filled($tmpPath) && filled($container) && filled($serverId)) {
if (str($tmpPath)->startsWith('/tmp/')
&& str($scriptPath)->startsWith('/tmp/')
&& ! str($tmpPath)->contains('..')
&& ! str($scriptPath)->contains('..')
&& strlen($tmpPath) > 5 // longer than just "/tmp/"
&& strlen($scriptPath) > 5
) {
$commands[] = "docker exec {$container} sh -c 'rm {$scriptPath}'";
$commands[] = "docker exec {$container} sh -c 'rm {$tmpPath}'";
instant_remote_process($commands, Server::find($serverId), throwError: true);
}
}
}
}

View File

@@ -18,6 +18,7 @@ use App\Models\Service;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -27,6 +28,9 @@ class ApplicationsController extends Controller
{ {
$application->makeHidden([ $application->makeHidden([
'id', 'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]); ]);
if (request()->attributes->get('can_read_sensitive', false) === false) { if (request()->attributes->get('can_read_sensitive', false) === false) {
$application->makeHidden([ $application->makeHidden([
@@ -114,11 +118,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
@@ -185,8 +190,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -220,11 +234,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
@@ -291,8 +306,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -326,11 +350,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'], 'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'],
'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'],
'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'],
@@ -397,8 +422,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -432,11 +466,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'dockerfile'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'dockerfile'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'],
'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'],
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
@@ -487,8 +522,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -522,11 +566,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_registry_image_name', 'ports_exposes'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_registry_image_name', 'ports_exposes'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'],
'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'],
'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'],
@@ -574,8 +619,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -609,11 +663,12 @@ class ApplicationsController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_compose_raw'], required: ['project_uuid', 'server_uuid', 'environment_name', 'environment_uuid', 'docker_compose_raw'],
properties: [ properties: [
'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'],
'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'],
'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'], 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'],
'name' => ['type' => 'string', 'description' => 'The application name.'], 'name' => ['type' => 'string', 'description' => 'The application name.'],
@@ -627,8 +682,17 @@ class ApplicationsController extends Controller
), ),
responses: [ responses: [
new OA\Response( new OA\Response(
response: 200, response: 201,
description: 'Application created successfully.', description: 'Application created successfully.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
)
), ),
new OA\Response( new OA\Response(
response: 401, response: 401,
@@ -647,7 +711,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type) private function create_application(Request $request, $type)
{ {
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', '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', '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', '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']; $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', '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', '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', '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'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
@@ -661,7 +725,8 @@ class ApplicationsController extends Controller
'name' => 'string|max:255', 'name' => 'string|max:255',
'description' => 'string|nullable', 'description' => 'string|nullable',
'project_uuid' => 'string|required', 'project_uuid' => 'string|required',
'environment_name' => 'string|required', 'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required', 'server_uuid' => 'string|required',
'destination_uuid' => 'string', 'destination_uuid' => 'string',
]); ]);
@@ -681,6 +746,11 @@ class ApplicationsController extends Controller
], 422); ], 422);
} }
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid; $serverUuid = $request->server_uuid;
$fqdn = $request->domains; $fqdn = $request->domains;
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;
@@ -713,7 +783,10 @@ class ApplicationsController extends Controller
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$environment = $project->environments()->where('name', $request->environment_name)->first(); $environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) { if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404); return response()->json(['message' => 'Environment not found.'], 404);
} }
@@ -730,12 +803,6 @@ class ApplicationsController extends Controller
} }
$destination = $destinations->first(); $destination = $destinations->first();
if ($type === 'public') { if ($type === 'public') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [ $validationRules = [
'git_repository' => 'string|required', 'git_repository' => 'string|required',
'git_branch' => 'string|required', 'git_branch' => 'string|required',
@@ -745,7 +812,12 @@ class ApplicationsController extends Controller
'docker_compose_raw' => 'string|nullable', 'docker_compose_raw' => 'string|nullable',
'docker_compose_domains' => 'array|nullable', 'docker_compose_domains' => 'array|nullable',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); // ports_exposes is not required for dockercompose
if ($request->build_pack === 'dockercompose') {
$validationRules['ports_exposes'] = 'string';
$request->offsetSet('ports_exposes', '80');
}
$validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
return response()->json([ return response()->json([
@@ -753,7 +825,9 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -776,7 +850,13 @@ class ApplicationsController extends Controller
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
$application->docker_compose_domains = $dockerComposeDomainsJson; $application->docker_compose_domains = $dockerComposeDomainsJson;
} }
$repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost();
if ($git_host === 'github.com') {
$application->source_type = GithubApp::class;
$application->source_id = GithubApp::find(0)->id;
}
$application->git_repository = $repository_url_parsed->getSegment(1).'/'.$repository_url_parsed->getSegment(2);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
@@ -791,7 +871,7 @@ class ApplicationsController extends Controller
$application->settings->save(); $application->settings->save();
} }
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save(); $application->save();
} }
@@ -815,14 +895,8 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'), 'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'), 'domains' => data_get($application, 'domains'),
])); ]))->setStatusCode(201);
} elseif ($type === 'private-gh-app') { } elseif ($type === 'private-gh-app') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [ $validationRules = [
'git_repository' => 'string|required', 'git_repository' => 'string|required',
'git_branch' => 'string|required', 'git_branch' => 'string|required',
@@ -833,7 +907,7 @@ class ApplicationsController extends Controller
'docker_compose_location' => 'string', 'docker_compose_location' => 'string',
'docker_compose_raw' => 'string|nullable', 'docker_compose_raw' => 'string|nullable',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
@@ -842,6 +916,14 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -884,13 +966,13 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass(); $application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id; $application->source_id = $githubApp->id;
$application->save();
$application->refresh();
if (isset($useBuildServer)) { if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer; $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save(); $application->settings->save();
} }
$application->save(); if ($application->settings->is_container_label_readonly_enabled) {
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save(); $application->save();
} }
@@ -914,14 +996,8 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'), 'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'), 'domains' => data_get($application, 'domains'),
])); ]))->setStatusCode(201);
} elseif ($type === 'private-deploy-key') { } elseif ($type === 'private-deploy-key') {
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$validationRules = [ $validationRules = [
'git_repository' => 'string|required', 'git_repository' => 'string|required',
@@ -934,7 +1010,7 @@ class ApplicationsController extends Controller
'docker_compose_raw' => 'string|nullable', 'docker_compose_raw' => 'string|nullable',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
@@ -943,6 +1019,13 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
if (! $request->has('name')) {
$request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch));
}
if ($request->build_pack === 'dockercompose') {
$request->offsetSet('ports_exposes', '80');
}
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -980,13 +1063,13 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->save();
$application->refresh();
if (isset($useBuildServer)) { if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer; $application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save(); $application->settings->save();
} }
$application->save(); if ($application->settings->is_container_label_readonly_enabled) {
$application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save(); $application->save();
} }
@@ -1010,16 +1093,12 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'), 'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'), 'domains' => data_get($application, 'domains'),
])); ]))->setStatusCode(201);
} elseif ($type === 'dockerfile') { } elseif ($type === 'dockerfile') {
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$validationRules = [ $validationRules = [
'dockerfile' => 'string|required', 'dockerfile' => 'string|required',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
@@ -1028,6 +1107,10 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
if (! $request->has('name')) {
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
}
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -1066,16 +1149,16 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save(); $application->save();
} }
@@ -1095,17 +1178,14 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'), 'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'), 'domains' => data_get($application, 'domains'),
])); ]))->setStatusCode(201);
} elseif ($type === 'dockerimage') { } elseif ($type === 'dockerimage') {
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$validationRules = [ $validationRules = [
'docker_registry_image_name' => 'string|required', 'docker_registry_image_name' => 'string|required',
'docker_registry_image_tag' => 'string', 'docker_registry_image_tag' => 'string',
'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
@@ -1114,6 +1194,9 @@ class ApplicationsController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
if (! $request->has('name')) {
$request->offsetSet('name', 'docker-image-'.new Cuid2);
}
$return = $this->validateDataApplications($request, $server); $return = $this->validateDataApplications($request, $server);
if ($return instanceof \Illuminate\Http\JsonResponse) { if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return; return $return;
@@ -1130,16 +1213,16 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
if ($application->settings->is_container_label_readonly_enabled) {
$application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n");
$application->save(); $application->save();
} }
@@ -1159,9 +1242,9 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($application, 'uuid'), 'uuid' => data_get($application, 'uuid'),
'domains' => data_get($application, 'domains'), 'domains' => data_get($application, 'domains'),
])); ]))->setStatusCode(201);
} elseif ($type === 'dockercompose') { } elseif ($type === 'dockercompose') {
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw']; $allowedFields = ['project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw'];
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) { if ($validator->fails() || ! empty($extraFields)) {
@@ -1183,7 +1266,7 @@ class ApplicationsController extends Controller
$validationRules = [ $validationRules = [
'docker_compose_raw' => 'string|required', 'docker_compose_raw' => 'string|required',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
if ($validator->fails()) { if ($validator->fails()) {
@@ -1216,11 +1299,6 @@ class ApplicationsController extends Controller
$dockerCompose = base64_decode($request->docker_compose_raw); $dockerCompose = base64_decode($request->docker_compose_raw);
$dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
// $isValid = validateComposeFile($dockerComposeRaw, $server_id);
// if ($isValid !== 'OK') {
// return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
// }
$service = new Service; $service = new Service;
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$service->fill($request->all()); $service->fill($request->all());
@@ -1241,7 +1319,7 @@ class ApplicationsController extends Controller
return response()->json(serializeApiResponse([ return response()->json(serializeApiResponse([
'uuid' => data_get($service, 'uuid'), 'uuid' => data_get($service, 'uuid'),
'domains' => data_get($service, 'domains'), 'domains' => data_get($service, 'domains'),
])); ]))->setStatusCode(201);
} }
return response()->json(['message' => 'Invalid type.'], 400); return response()->json(['message' => 'Invalid type.'], 400);
@@ -1313,6 +1391,108 @@ class ApplicationsController extends Controller
return response()->json($this->removeSensitiveData($application)); return response()->json($this->removeSensitiveData($application));
} }
#[OA\Get(
summary: 'Get application logs.',
description: 'Get application logs by UUID.',
path: '/applications/{uuid}/logs',
operationId: 'get-application-logs-by-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
new OA\Parameter(
name: 'lines',
in: 'query',
description: 'Number of lines to show from the end of the logs.',
required: false,
schema: new OA\Schema(
type: 'integer',
format: 'int32',
default: 100,
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Get application logs by UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'logs' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function logs_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id);
if ($containers->count() == 0) {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$container = $containers->first();
$status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$lines = $request->query->get('lines', 100) ?: 100;
$logs = getContainerLogs($application->destination->server, $container['ID'], $lines);
return response()->json([
'logs' => $logs,
]);
}
#[OA\Delete( #[OA\Delete(
summary: 'Delete', summary: 'Delete',
description: 'Delete application by UUID.', description: 'Delete application by UUID.',
@@ -1551,7 +1731,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => 'string|nullable', 'docker_compose_custom_build_command' => 'string|nullable',
'custom_nginx_configuration' => 'string|nullable', 'custom_nginx_configuration' => 'string|nullable',
]; ];
$validationRules = array_merge($validationRules, sharedDataApplications()); $validationRules = array_merge(sharedDataApplications(), $validationRules);
$validator = customApiValidator($request->all(), $validationRules); $validator = customApiValidator($request->all(), $validationRules);
// Validate ports_exposes // Validate ports_exposes
@@ -1606,7 +1786,8 @@ class ApplicationsController extends Controller
], 422); ], 422);
} }
$domains = $request->domains; $domains = $request->domains;
if ($request->has('domains') && $server->isProxyShouldRun()) { $requestHasDomains = $request->has('domains');
if ($requestHasDomains && $server->isProxyShouldRun()) {
$uuid = $request->uuid; $uuid = $request->uuid;
$fqdn = $request->domains; $fqdn = $request->domains;
$fqdn = str($fqdn)->replaceEnd(',', '')->trim(); $fqdn = str($fqdn)->replaceEnd(',', '')->trim();
@@ -1668,7 +1849,10 @@ class ApplicationsController extends Controller
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$data = $request->all(); $data = $request->all();
if ($requestHasDomains && $server->isProxyShouldRun()) {
data_set($data, 'fqdn', $domains); data_set($data, 'fqdn', $domains);
}
if ($dockerComposeDomainsJson->count() > 0) { if ($dockerComposeDomainsJson->count() > 0) {
data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson)); data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
} }
@@ -1893,8 +2077,9 @@ class ApplicationsController extends Controller
$is_preview = $request->is_preview ?? false; $is_preview = $request->is_preview ?? false;
$is_build_time = $request->is_build_time ?? false; $is_build_time = $request->is_build_time ?? false;
$is_literal = $request->is_literal ?? false; $is_literal = $request->is_literal ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) { if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first(); $env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) { if ($env) {
$env->value = $request->value; $env->value = $request->value;
if ($env->is_build_time != $is_build_time) { if ($env->is_build_time != $is_build_time) {
@@ -1921,7 +2106,7 @@ class ApplicationsController extends Controller
], 404); ], 404);
} }
} else { } else {
$env = $application->environment_variables->where('key', $request->key)->first(); $env = $application->environment_variables->where('key', $key)->first();
if ($env) { if ($env) {
$env->value = $request->value; $env->value = $request->value;
if ($env->is_build_time != $is_build_time) { if ($env->is_build_time != $is_build_time) {
@@ -2064,6 +2249,7 @@ class ApplicationsController extends Controller
$bulk_data = collect($bulk_data)->map(function ($item) { $bulk_data = collect($bulk_data)->map(function ($item) {
return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']);
}); });
$returnedEnvs = collect();
foreach ($bulk_data as $item) { foreach ($bulk_data as $item) {
$validator = customApiValidator($item, [ $validator = customApiValidator($item, [
'key' => 'string|required', 'key' => 'string|required',
@@ -2085,8 +2271,9 @@ class ApplicationsController extends Controller
$is_literal = $item->get('is_literal') ?? false; $is_literal = $item->get('is_literal') ?? false;
$is_multi_line = $item->get('is_multiline') ?? false; $is_multi_line = $item->get('is_multiline') ?? false;
$is_shown_once = $item->get('is_shown_once') ?? false; $is_shown_once = $item->get('is_shown_once') ?? false;
$key = str($item->get('key'))->trim()->replace(' ', '_')->value;
if ($is_preview) { if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $item->get('key'))->first(); $env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) { if ($env) {
$env->value = $item->get('value'); $env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) { if ($env->is_build_time != $is_build_time) {
@@ -2111,10 +2298,12 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal, 'is_literal' => $is_literal,
'is_multiline' => $is_multi_line, 'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once, 'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]); ]);
} }
} else { } else {
$env = $application->environment_variables->where('key', $item->get('key'))->first(); $env = $application->environment_variables->where('key', $key)->first();
if ($env) { if ($env) {
$env->value = $item->get('value'); $env->value = $item->get('value');
if ($env->is_build_time != $is_build_time) { if ($env->is_build_time != $is_build_time) {
@@ -2139,12 +2328,15 @@ class ApplicationsController extends Controller
'is_literal' => $is_literal, 'is_literal' => $is_literal,
'is_multiline' => $is_multi_line, 'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once, 'is_shown_once' => $is_shown_once,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]); ]);
} }
} }
$returnedEnvs->push($this->removeSensitiveData($env));
} }
return response()->json($this->removeSensitiveData($env))->setStatusCode(201); return response()->json($returnedEnvs)->setStatusCode(201);
} }
#[OA\Post( #[OA\Post(
@@ -2257,8 +2449,10 @@ class ApplicationsController extends Controller
], 422); ], 422);
} }
$is_preview = $request->is_preview ?? false; $is_preview = $request->is_preview ?? false;
$key = str($request->key)->trim()->replace(' ', '_')->value;
if ($is_preview) { if ($is_preview) {
$env = $application->environment_variables_preview->where('key', $request->key)->first(); $env = $application->environment_variables_preview->where('key', $key)->first();
if ($env) { if ($env) {
return response()->json([ return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.', 'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2272,6 +2466,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false, 'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false, 'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false, 'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]); ]);
return response()->json([ return response()->json([
@@ -2279,7 +2475,7 @@ class ApplicationsController extends Controller
])->setStatusCode(201); ])->setStatusCode(201);
} }
} else { } else {
$env = $application->environment_variables->where('key', $request->key)->first(); $env = $application->environment_variables->where('key', $key)->first();
if ($env) { if ($env) {
return response()->json([ return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.', 'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -2293,6 +2489,8 @@ class ApplicationsController extends Controller
'is_literal' => $request->is_literal ?? false, 'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false, 'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false, 'is_shown_once' => $request->is_shown_once ?? false,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]); ]);
return response()->json([ return response()->json([
@@ -2380,7 +2578,10 @@ class ApplicationsController extends Controller
'message' => 'Application not found.', 'message' => 'Application not found.',
], 404); ], 404);
} }
$found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first(); $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('resourceable_type', Application::class)
->where('resourceable_id', $application->id)
->first();
if (! $found_env) { if (! $found_env) {
return response()->json([ return response()->json([
'message' => 'Environment variable not found.', 'message' => 'Environment variable not found.',

View File

@@ -523,11 +523,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'], 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'],
'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'], 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'],
'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'], 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'],
@@ -589,11 +590,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'], 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'],
'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'], 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'],
@@ -651,11 +653,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'], 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'],
'name' => ['type' => 'string', 'description' => 'Name of the database'], 'name' => ['type' => 'string', 'description' => 'Name of the database'],
@@ -712,11 +715,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'redis_password' => ['type' => 'string', 'description' => 'Redis password'], 'redis_password' => ['type' => 'string', 'description' => 'Redis password'],
'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'], 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'],
@@ -774,11 +778,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'], 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'],
'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'], 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'],
@@ -836,11 +841,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'], 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'],
'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'], 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'],
@@ -901,11 +907,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'],
'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'], 'mysql_password' => ['type' => 'string', 'description' => 'MySQL password'],
@@ -966,11 +973,12 @@ class DatabasesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [ properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'], 'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'],
'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'], 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'],
@@ -1013,7 +1021,7 @@ class DatabasesController extends Controller
public function create_database(Request $request, NewDatabaseTypes $type) public function create_database(Request $request, NewDatabaseTypes $type)
{ {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -1039,6 +1047,11 @@ class DatabasesController extends Controller
'errors' => $errors, 'errors' => $errors,
], 422); ], 422);
} }
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid; $serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false; $instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) { if ($request->is_public && ! $request->public_port) {
@@ -1048,9 +1061,12 @@ class DatabasesController extends Controller
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$environment = $project->environments()->where('name', $request->environment_name)->first(); $environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) { if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404); $environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) {
return response()->json(['message' => 'You need to provide a valid environment_name or environment_uuid.'], 422);
} }
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
if (! $server) { if (! $server) {
@@ -1074,7 +1090,8 @@ class DatabasesController extends Controller
'description' => 'string|nullable', 'description' => 'string|nullable',
'image' => 'string', 'image' => 'string',
'project_uuid' => 'string|required', 'project_uuid' => 'string|required',
'environment_name' => 'string|required', 'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required', 'server_uuid' => 'string|required',
'destination_uuid' => 'string', 'destination_uuid' => 'string',
'is_public' => 'boolean', 'is_public' => 'boolean',
@@ -1105,7 +1122,7 @@ class DatabasesController extends Controller
} }
} }
if ($type === NewDatabaseTypes::POSTGRESQL) { if ($type === NewDatabaseTypes::POSTGRESQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'postgres_user' => 'string', 'postgres_user' => 'string',
'postgres_password' => 'string', 'postgres_password' => 'string',
@@ -1164,7 +1181,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MARIADB) { } elseif ($type === NewDatabaseTypes::MARIADB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string', 'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string', 'clickhouse_admin_password' => 'string',
@@ -1220,7 +1237,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MYSQL) { } elseif ($type === NewDatabaseTypes::MYSQL) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mysql_root_password' => 'string', 'mysql_root_password' => 'string',
'mysql_password' => 'string', 'mysql_password' => 'string',
@@ -1279,7 +1296,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::REDIS) { } elseif ($type === NewDatabaseTypes::REDIS) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'redis_password' => 'string', 'redis_password' => 'string',
'redis_conf' => 'string', 'redis_conf' => 'string',
@@ -1335,7 +1352,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::DRAGONFLY) { } elseif ($type === NewDatabaseTypes::DRAGONFLY) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'dragonfly_password' => 'string', 'dragonfly_password' => 'string',
]); ]);
@@ -1365,7 +1382,7 @@ class DatabasesController extends Controller
'uuid' => $database->uuid, 'uuid' => $database->uuid,
]))->setStatusCode(201); ]))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::KEYDB) { } elseif ($type === NewDatabaseTypes::KEYDB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'keydb_password' => 'string', 'keydb_password' => 'string',
'keydb_conf' => 'string', 'keydb_conf' => 'string',
@@ -1421,7 +1438,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::CLICKHOUSE) { } elseif ($type === NewDatabaseTypes::CLICKHOUSE) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'clickhouse_admin_user' => 'string', 'clickhouse_admin_user' => 'string',
'clickhouse_admin_password' => 'string', 'clickhouse_admin_password' => 'string',
@@ -1457,7 +1474,7 @@ class DatabasesController extends Controller
return response()->json(serializeApiResponse($payload))->setStatusCode(201); return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::MONGODB) { } elseif ($type === NewDatabaseTypes::MONGODB) {
$allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database']; $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'mongo_conf' => 'string', 'mongo_conf' => 'string',
'mongo_initdb_root_username' => 'string', 'mongo_initdb_root_username' => 'string',

View File

@@ -90,11 +90,13 @@ class ProjectController extends Controller
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
} }
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$project->load(['environments']);
return response()->json( return response()->json(
serializeApiResponse($project), serializeApiResponse($project),
); );
@@ -102,16 +104,16 @@ class ProjectController extends Controller
#[OA\Get( #[OA\Get(
summary: 'Environment', summary: 'Environment',
description: 'Get environment by name.', description: 'Get environment by name or UUID.',
path: '/projects/{uuid}/{environment_name}', path: '/projects/{uuid}/{environment_name_or_uuid}',
operationId: 'get-environment-by-name', operationId: 'get-environment-by-name-or-uuid',
security: [ security: [
['bearerAuth' => []], ['bearerAuth' => []],
], ],
tags: ['Projects'], tags: ['Projects'],
parameters: [ parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'environment_name_or_uuid', in: 'path', required: true, description: 'Environment name or UUID', schema: new OA\Schema(type: 'string')),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@@ -141,14 +143,17 @@ class ProjectController extends Controller
if (! $request->uuid) { if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 422); return response()->json(['message' => 'UUID is required.'], 422);
} }
if (! $request->environment_name) { if (! $request->environment_name_or_uuid) {
return response()->json(['message' => 'Environment name is required.'], 422); return response()->json(['message' => 'Environment name or UUID is required.'], 422);
} }
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first(); $project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$environment = $project->environments()->whereName($request->environment_name)->first(); $environment = $project->environments()->whereName($request->environment_name_or_uuid)->first();
if (! $environment) {
$environment = $project->environments()->whereUuid($request->environment_name_or_uuid)->first();
}
if (! $environment) { if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404); return response()->json(['message' => 'Environment not found.'], 404);
} }

View File

@@ -195,6 +195,31 @@ class SecurityController extends Controller
if (! $request->description) { if (! $request->description) {
$request->offsetSet('description', 'Created by Coolify via API'); $request->offsetSet('description', 'Created by Coolify via API');
} }
$isPrivateKeyString = str_starts_with($request->private_key, '-----BEGIN');
if (! $isPrivateKeyString) {
try {
$base64PrivateKey = base64_decode($request->private_key);
$request->offsetSet('private_key', $base64PrivateKey);
} catch (\Exception $e) {
return response()->json([
'message' => 'Invalid private key.',
], 422);
}
}
$isPrivateKeyValid = PrivateKey::validatePrivateKey($request->private_key);
if (! $isPrivateKeyValid) {
return response()->json([
'message' => 'Invalid private key.',
], 422);
}
$fingerPrint = PrivateKey::generateFingerprint($request->private_key);
$isFingerPrintExists = PrivateKey::fingerprintExists($fingerPrint);
if ($isFingerPrintExists) {
return response()->json([
'message' => 'Private key already exists.',
], 422);
}
$key = PrivateKey::create([ $key = PrivateKey::create([
'team_id' => $teamId, 'team_id' => $teamId,
'name' => $request->name, 'name' => $request->name,

View File

@@ -530,11 +530,11 @@ class ServersController extends Controller
'user' => $request->user, 'user' => $request->user,
'private_key_id' => $privateKey->id, 'private_key_id' => $privateKey->id,
'team_id' => $teamId, 'team_id' => $teamId,
'proxy' => [
'type' => $proxyType,
'status' => ProxyStatus::EXITED->value,
],
]); ]);
$server->proxy->set('type', $proxyType);
$server->proxy->set('status', ProxyStatus::EXITED->value);
$server->save();
$server->settings()->update([ $server->settings()->update([
'is_build_server' => $request->is_build_server, 'is_build_server' => $request->is_build_server,
]); ]);
@@ -742,6 +742,9 @@ class ServersController extends Controller
if ($server->definedResources()->count() > 0) { if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
} }
if ($server->isLocalhost()) {
return response()->json(['message' => 'Local server cannot be deleted.'], 400);
}
$server->delete(); $server->delete();
DeleteServer::dispatch($server); DeleteServer::dispatch($server);

View File

@@ -20,6 +20,9 @@ class ServicesController extends Controller
{ {
$service->makeHidden([ $service->makeHidden([
'id', 'id',
'resourceable',
'resourceable_id',
'resourceable_type',
]); ]);
if (request()->attributes->get('can_read_sensitive', false) === false) { if (request()->attributes->get('can_read_sensitive', false) === false) {
$service->makeHidden([ $service->makeHidden([
@@ -99,7 +102,7 @@ class ServicesController extends Controller
mediaType: 'application/json', mediaType: 'application/json',
schema: new OA\Schema( schema: new OA\Schema(
type: 'object', type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'type'], required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid', 'type'],
properties: [ properties: [
'type' => [ 'type' => [
'description' => 'The one-click service type', 'description' => 'The one-click service type',
@@ -196,7 +199,8 @@ class ServicesController extends Controller
'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'], 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'], 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'], 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'Environment name.'], 'environment_name' => ['type' => 'string', 'description' => 'Environment name. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'Environment UUID. You need to provide at least one of environment_name or environment_uuid.'],
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'], 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'], 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'], 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
@@ -233,7 +237,7 @@ class ServicesController extends Controller
)] )]
public function create_service(Request $request) public function create_service(Request $request)
{ {
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy']; $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
@@ -247,7 +251,8 @@ class ServicesController extends Controller
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
'type' => 'string|required', 'type' => 'string|required',
'project_uuid' => 'string|required', 'project_uuid' => 'string|required',
'environment_name' => 'string|required', 'environment_name' => 'string|nullable',
'environment_uuid' => 'string|nullable',
'server_uuid' => 'string|required', 'server_uuid' => 'string|required',
'destination_uuid' => 'string', 'destination_uuid' => 'string',
'name' => 'string|max:255', 'name' => 'string|max:255',
@@ -269,6 +274,11 @@ class ServicesController extends Controller
'errors' => $errors, 'errors' => $errors,
], 422); ], 422);
} }
$environmentUuid = $request->environment_uuid;
$environmentName = $request->environment_name;
if (blank($environmentUuid) && blank($environmentName)) {
return response()->json(['message' => 'You need to provide at least one of environment_name or environment_uuid.'], 422);
}
$serverUuid = $request->server_uuid; $serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false; $instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) { if ($request->is_public && ! $request->public_port) {
@@ -278,7 +288,10 @@ class ServicesController extends Controller
if (! $project) { if (! $project) {
return response()->json(['message' => 'Project not found.'], 404); return response()->json(['message' => 'Project not found.'], 404);
} }
$environment = $project->environments()->where('name', $request->environment_name)->first(); $environment = $project->environments()->where('name', $environmentName)->first();
if (! $environment) {
$environment = $project->environments()->where('uuid', $environmentUuid)->first();
}
if (! $environment) { if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404); return response()->json(['message' => 'Environment not found.'], 404);
} }
@@ -333,7 +346,8 @@ class ServicesController extends Controller
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
'value' => $generatedValue, 'value' => $generatedValue,
'service_id' => $service->id, 'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
]); ]);
@@ -345,7 +359,11 @@ class ServicesController extends Controller
} }
$domains = $service->applications()->get()->pluck('fqdn')->sort(); $domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) { $domains = $domains->map(function ($domain) {
if (count(explode(':', $domain)) > 2) {
return str($domain)->beforeLast(':')->value(); return str($domain)->beforeLast(':')->value();
}
return $domain;
}); });
return response()->json([ return response()->json([
@@ -673,7 +691,8 @@ class ServicesController extends Controller
], 422); ], 422);
} }
$env = $service->environment_variables()->where('key', $request->key)->first(); $key = str($request->key)->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->where('key', $key)->first();
if (! $env) { if (! $env) {
return response()->json(['message' => 'Environment variable not found.'], 404); return response()->json(['message' => 'Environment variable not found.'], 404);
} }
@@ -799,9 +818,9 @@ class ServicesController extends Controller
'errors' => $validator->errors(), 'errors' => $validator->errors(),
], 422); ], 422);
} }
$key = str($item['key'])->trim()->replace(' ', '_')->value;
$env = $service->environment_variables()->updateOrCreate( $env = $service->environment_variables()->updateOrCreate(
['key' => $item['key']], ['key' => $key],
$item $item
); );
@@ -909,7 +928,8 @@ class ServicesController extends Controller
], 422); ], 422);
} }
$existingEnv = $service->environment_variables()->where('key', $request->key)->first(); $key = str($request->key)->trim()->replace(' ', '_')->value;
$existingEnv = $service->environment_variables()->where('key', $key)->first();
if ($existingEnv) { if ($existingEnv) {
return response()->json([ return response()->json([
'message' => 'Environment variable already exists. Use PATCH request to update it.', 'message' => 'Environment variable already exists. Use PATCH request to update it.',
@@ -995,7 +1015,8 @@ class ServicesController extends Controller
} }
$env = EnvironmentVariable::where('uuid', $request->env_uuid) $env = EnvironmentVariable::where('uuid', $request->env_uuid)
->where('service_id', $service->id) ->where('resourceable_type', Service::class)
->where('resourceable_id', $service->id)
->first(); ->first();
if (! $env) { if (! $env) {

View File

@@ -54,7 +54,7 @@ class Controller extends BaseController
'email' => Str::lower($arrayOfRequest['email']), 'email' => Str::lower($arrayOfRequest['email']),
]); ]);
$type = set_transanctional_email_settings(); $type = set_transanctional_email_settings();
if (! $type) { if (blank($type)) {
return response()->json(['message' => 'Transactional emails are not active'], 400); return response()->json(['message' => 'Transactional emails are not active'], 400);
} }
$request->validate([Fortify::email() => 'required|email']); $request->validate([Fortify::email() => 'required|email']);

View File

@@ -37,7 +37,7 @@ class Bitbucket extends Controller
$headers = $request->headers->all(); $headers = $request->headers->all();
$x_bitbucket_token = data_get($headers, 'x-hub-signature.0', ''); $x_bitbucket_token = data_get($headers, 'x-hub-signature.0', '');
$x_bitbucket_event = data_get($headers, 'x-event-key.0', ''); $x_bitbucket_event = data_get($headers, 'x-event-key.0', '');
$handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']); $handled_events = collect(['repo:push', 'pullrequest:updated', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']);
if (! $handled_events->contains($x_bitbucket_event)) { if (! $handled_events->contains($x_bitbucket_event)) {
return response([ return response([
'status' => 'failed', 'status' => 'failed',
@@ -48,6 +48,7 @@ class Bitbucket extends Controller
$branch = data_get($payload, 'push.changes.0.new.name'); $branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name'); $full_name = data_get($payload, 'repository.full_name');
$commit = data_get($payload, 'push.changes.0.new.target.hash'); $commit = data_get($payload, 'push.changes.0.new.target.hash');
if (! $branch) { if (! $branch) {
return response([ return response([
'status' => 'failed', 'status' => 'failed',
@@ -55,7 +56,7 @@ class Bitbucket extends Controller
]); ]);
} }
} }
if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { if ($x_bitbucket_event === 'pullrequest:updated' || $x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') {
$branch = data_get($payload, 'pullrequest.destination.branch.name'); $branch = data_get($payload, 'pullrequest.destination.branch.name');
$base_branch = data_get($payload, 'pullrequest.source.branch.name'); $base_branch = data_get($payload, 'pullrequest.source.branch.name');
$full_name = data_get($payload, 'repository.full_name'); $full_name = data_get($payload, 'repository.full_name');
@@ -119,7 +120,7 @@ class Bitbucket extends Controller
]); ]);
} }
} }
if ($x_bitbucket_event === 'pullrequest:created') { if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:updated') {
if ($application->isPRDeployable()) { if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();

View File

@@ -152,7 +152,7 @@ class Gitea extends Controller
} }
} }
if ($x_gitea_event === 'pull_request') { if ($x_gitea_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { if ($action === 'opened' || $action === 'synchronized' || $action === 'reopened') {
if ($application->isPRDeployable()) { if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
@@ -202,7 +202,6 @@ class Gitea extends Controller
if ($found) { if ($found) {
$found->delete(); $found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id); $container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server); instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -208,7 +208,6 @@ class Github extends Controller
if ($found) { if ($found) {
$found->delete(); $found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id); $container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server); instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -227,7 +227,6 @@ class Gitlab extends Controller
if ($found) { if ($found) {
$found->delete(); $found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id); $container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server); instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$return_payloads->push([ $return_payloads->push([
'application' => $application->name, 'application' => $application->name,

View File

@@ -18,6 +18,7 @@ use App\Models\SwarmDocker;
use App\Notifications\Application\DeploymentFailed; use App\Notifications\Application\DeploymentFailed;
use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\DeploymentSuccess;
use App\Traits\ExecuteRemoteCommand; use App\Traits\ExecuteRemoteCommand;
use Carbon\Carbon;
use Exception; use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -39,12 +40,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
{ {
use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 3600; public $timeout = 3600;
public static int $batch_counter = 0; public static int $batch_counter = 0;
private int $application_deployment_queue_id;
private bool $newVersionIsHealthy = false; private bool $newVersionIsHealthy = false;
private ApplicationDeploymentQueue $application_deployment_queue; private ApplicationDeploymentQueue $application_deployment_queue;
@@ -126,6 +127,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $nixpacks_plan = null; private ?string $nixpacks_plan = null;
private Collection $nixpacks_plan_json;
private ?string $nixpacks_type = null; private ?string $nixpacks_type = null;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
@@ -164,18 +167,23 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $preserveRepository = false; private bool $preserveRepository = false;
public $tries = 1; public function tags()
{
// Do not remove this one, it needs to properly identify which worker is running the job
return ['App\Models\ApplicationDeploymentQueue:'.$this->application_deployment_queue_id];
}
public function __construct(int $application_deployment_queue_id) public function __construct(public int $application_deployment_queue_id)
{ {
$this->onQueue('high'); $this->onQueue('high');
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($this->application_deployment_queue_id);
$this->nixpacks_plan_json = collect([]);
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack'); $this->build_pack = data_get($this->application, 'build_pack');
$this->build_args = collect([]); $this->build_args = collect([]);
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
$this->pull_request_id = $this->application_deployment_queue->pull_request_id; $this->pull_request_id = $this->application_deployment_queue->pull_request_id;
$this->commit = $this->application_deployment_queue->commit; $this->commit = $this->application_deployment_queue->commit;
@@ -233,15 +241,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
public function tags(): array
{
return ['server:'.gethostname()];
}
public function handle(): void public function handle(): void
{ {
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
'horizon_job_worker' => gethostname(),
]); ]);
if ($this->server->isFunctional() === false) { if ($this->server->isFunctional() === false) {
$this->application_deployment_queue->addLogEntry('Server is not functional.'); $this->application_deployment_queue->addLogEntry('Server is not functional.');
@@ -250,6 +254,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return; return;
} }
try { try {
// Make sure the private key is stored in the filesystem
$this->server->privateKey->storeInFileSystem();
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
@@ -313,6 +320,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->fail($e); $this->fail($e);
throw $e; throw $e;
} finally { } finally {
$this->application_deployment_queue->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
if ($this->use_build_server) { if ($this->use_build_server) {
$this->server = $this->build_server; $this->server = $this->build_server;
} else { } else {
@@ -916,8 +927,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
} }
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
} }
} }
@@ -975,8 +989,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\""); $envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
} }
if ($this->application->environment_variables->where('key', 'COOLIFY_RESOURCE_UUID')->isEmpty()) {
$envs->push("COOLIFY_RESOURCE_UUID={$this->application->uuid}");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\""); $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
} }
} }
@@ -1115,7 +1132,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
$nixpacks_php_fallback_path->value = '/index.php'; $nixpacks_php_fallback_path->value = '/index.php';
$nixpacks_php_fallback_path->is_build_time = false; $nixpacks_php_fallback_path->is_build_time = false;
$nixpacks_php_fallback_path->application_id = $this->application->id; $nixpacks_php_fallback_path->resourceable_id = $this->application->id;
$nixpacks_php_fallback_path->resourceable_type = 'App\Models\Application';
$nixpacks_php_fallback_path->save(); $nixpacks_php_fallback_path->save();
} }
if (! $nixpacks_php_root_dir) { if (! $nixpacks_php_root_dir) {
@@ -1123,7 +1141,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
$nixpacks_php_root_dir->value = '/app/public'; $nixpacks_php_root_dir->value = '/app/public';
$nixpacks_php_root_dir->is_build_time = false; $nixpacks_php_root_dir->is_build_time = false;
$nixpacks_php_root_dir->application_id = $this->application->id; $nixpacks_php_root_dir->resourceable_id = $this->application->id;
$nixpacks_php_root_dir->resourceable_type = 'App\Models\Application';
$nixpacks_php_root_dir->save(); $nixpacks_php_root_dir->save();
} }
@@ -1136,7 +1155,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry('Rolling update started.'); $this->application_deployment_queue->addLogEntry('Rolling update started.');
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "docker stack deploy --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"), executeInDocker($this->deployment_uuid, "docker stack deploy --detach=true --with-registry-auth -c {$this->workdir}{$this->docker_compose_location} {$this->application->uuid}"),
], ],
); );
$this->application_deployment_queue->addLogEntry('Rolling update completed.'); $this->application_deployment_queue->addLogEntry('Rolling update completed.');
@@ -1189,7 +1208,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->custom_healthcheck_found) { if ($this->application->custom_healthcheck_found) {
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.'); $this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
} }
// ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$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.');
@@ -1392,7 +1410,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue; continue;
} }
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2; $deployment_uuid = new Cuid2;
queue_application_deployment( queue_application_deployment(
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
@@ -1405,7 +1422,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'project_uuid' => data_get($this->application, 'environment.project.uuid'), 'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'), 'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $deployment_uuid, 'deployment_uuid' => $deployment_uuid,
'environment_name' => data_get($this->application, 'environment.name'), 'environment_uuid' => data_get($this->application, 'environment.uuid'),
])); ]));
} }
} }
@@ -1494,7 +1511,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
] ]
); );
if ($this->saved_outputs->get('commit_message')) { if ($this->saved_outputs->get('commit_message')) {
$commit_message = str($this->saved_outputs->get('commit_message'))->limit(47); $commit_message = str($this->saved_outputs->get('commit_message'));
$this->application_deployment_queue->commit_message = $commit_message->value(); $this->application_deployment_queue->commit_message = $commit_message->value();
ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update( ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(
['commit_message' => $commit_message->value()] ['commit_message' => $commit_message->value()]
@@ -1545,7 +1562,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Do any modifications here // Do any modifications here
$this->generate_env_variables(); $this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args);
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
if (count($aptPkgs) === 0) { if (count($aptPkgs) === 0) {
$aptPkgs = ['curl', 'wget']; $aptPkgs = ['curl', 'wget'];
@@ -1570,6 +1587,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->elixir_finetunes(); $this->elixir_finetunes();
} }
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->nixpacks_plan_json = collect($parsed);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') { if ($this->nixpacks_type === 'rust') {
// temporary: disable healthcheck for rust because the start phase does not have curl/wget // temporary: disable healthcheck for rust because the start phase does not have curl/wget
@@ -1678,7 +1696,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application->custom_labels = base64_encode($labels->implode("\n")); $this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save(); $this->application->save();
} else { } else {
if (! $this->application->settings->is_container_label_readonly_enabled) { if ($this->application->settings->is_container_label_readonly_enabled) {
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
} }
@@ -1690,7 +1708,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
return escapeDollarSign($value); return escapeDollarSign($value);
}); });
} }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->application->project()->name, $this->application->name, $this->application->environment->name, $this->pull_request_id))->toArray();
// Check for custom HEALTHCHECK // Check for custom HEALTHCHECK
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) { if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
@@ -2005,6 +2023,8 @@ LABEL coolify.deploymentId={$this->deployment_uuid}
COPY . . COPY . .
RUN rm -f /usr/share/nginx/html/nginx.conf RUN rm -f /usr/share/nginx/html/nginx.conf
RUN rm -f /usr/share/nginx/html/Dockerfile RUN rm -f /usr/share/nginx/html/Dockerfile
RUN rm -f /usr/share/nginx/html/docker-compose.yaml
RUN rm -f /usr/share/nginx/html/.env
COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
if (str($this->application->custom_nginx_configuration)->isNotEmpty()) { if (str($this->application->custom_nginx_configuration)->isNotEmpty()) {
$nginx_config = base64_encode($this->application->custom_nginx_configuration); $nginx_config = base64_encode($this->application->custom_nginx_configuration);
@@ -2266,7 +2286,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} else { } else {
if ($this->use_build_server) { if ($this->use_build_server) {
$this->execute_remote_command( $this->execute_remote_command(
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true], ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
@@ -2279,18 +2299,18 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function generate_build_env_variables() private function generate_build_env_variables()
{ {
$this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]); if ($this->application->build_pack === 'nixpacks') {
if ($this->pull_request_id === 0) { $variables = collect($this->nixpacks_plan_json->get('variables'));
foreach ($this->application->build_environment_variables as $env) {
$value = escapeshellarg($env->real_value);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
} else { } else {
foreach ($this->application->build_environment_variables_preview as $env) { $this->generate_env_variables();
$value = escapeshellarg($env->real_value); $variables = collect([])->merge($this->env_args);
$this->build_args->push("--build-arg {$env->key}={$value}");
}
} }
$this->build_args = $variables->map(function ($value, $key) {
$value = escapeshellarg($value);
return "--build-arg {$key}={$value}";
});
} }
private function add_build_env_variables_to_dockerfile() private function add_build_env_variables_to_dockerfile()
@@ -2395,7 +2415,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
queue_next_deployment($this->application); queue_next_deployment($this->application);
// If the deployment is cancelled by the user, don't update the status // If the deployment is cancelled by the user, don't update the status
if ( if (
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value && $this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value $this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value &&
$this->application_deployment_queue->status !== ApplicationDeploymentStatus::FAILED->value
) { ) {
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => $status, 'status' => $status,

View File

@@ -24,7 +24,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
$latestVersion = get_latest_sentinel_version(); $latestVersion = get_latest_sentinel_version();
// Check if sentinel is running // Check if sentinel is running
$sentinelFound = instant_remote_process(['docker inspect coolify-sentinel'], $this->server, false); $sentinelFound = instant_remote_process_with_timeout(['docker inspect coolify-sentinel'], $this->server, false, 10);
$sentinelFoundJson = json_decode($sentinelFound, true); $sentinelFoundJson = json_decode($sentinelFound, true);
$sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited');
if ($sentinelStatus !== 'running') { if ($sentinelStatus !== 'running') {
@@ -33,7 +33,7 @@ class CheckAndStartSentinelJob implements ShouldBeEncrypted, ShouldQueue
return; return;
} }
// If sentinel is running, check if it needs an update // If sentinel is running, check if it needs an update
$runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); $runningVersion = instant_remote_process_with_timeout(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($runningVersion)) { if (empty($runningVersion)) {
$runningVersion = '0.0.0'; $runningVersion = '0.0.0';
} }

View File

@@ -20,11 +20,11 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
public function handle(): void public function handle(): void
{ {
try { try {
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false); $containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID'); $containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) { if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) { foreach ($containerIds as $containerId) {
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false);
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -32,8 +32,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public Server $server; public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database; public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database;
public ?string $container_name = null; public ?string $container_name = null;
@@ -58,10 +56,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public ?S3Storage $s3 = null; public ?S3Storage $s3 = null;
public function __construct($backup) public function __construct(public ScheduledDatabaseBackup $backup)
{ {
$this->onQueue('high'); $this->onQueue('high');
$this->backup = $backup;
} }
public function handle(): void public function handle(): void
@@ -302,7 +299,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
throw new \Exception('Unsupported database type'); throw new \Exception('Unsupported database type');
} }
$size = $this->calculate_size(); $size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) { if ($this->backup->save_s3) {
$this->upload_to_s3(); $this->upload_to_s3();
} }
@@ -326,12 +322,20 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database)); $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
} }
} }
if ($this->backup_log && $this->backup_log->status === 'success') {
removeOldBackups($this->backup);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw $e; throw $e;
} finally { } finally {
if ($this->team) { if ($this->team) {
BackupCreated::dispatch($this->team->id); BackupCreated::dispatch($this->team->id);
} }
if ($this->backup_log) {
$this->backup_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
} }
} }
@@ -342,9 +346,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($databaseWithCollections === 'all') { if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4')) { if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
} else { } else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --gzip --archive > $this->backup_location";
} }
} else { } else {
if (str($databaseWithCollections)->contains(':')) { if (str($databaseWithCollections)->contains(':')) {
@@ -357,15 +361,15 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
if ($collectionsToExclude->count() === 0) { if ($collectionsToExclude->count() === 0) {
if (str($this->database->image)->startsWith('mongo:4')) { if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --archive > $this->backup_location";
} else { } else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --archive > $this->backup_location";
} }
} else { } else {
if (str($this->database->image)->startsWith('mongo:4')) { if (str($this->database->image)->startsWith('mongo:4')) {
$commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} else { } else {
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location"; $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $databaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
} }
} }
} }
@@ -411,9 +415,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
if ($this->backup->dump_all) { if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location"; $commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
} else { } else {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; $commands[] = "docker exec $this->container_name mysqldump -u root -p\"{$this->database->mysql_root_password}\" $database > $this->backup_location";
} }
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
@@ -431,9 +435,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
if ($this->backup->dump_all) { if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location"; $commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
} else { } else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; $commands[] = "docker exec $this->container_name mariadb-dump -u root -p\"{$this->database->mariadb_root_password}\" $database > $this->backup_location";
} }
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
@@ -460,19 +464,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false); return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
} }
private function remove_old_backups(): void
{
if ($this->backup->number_of_backups_locally === 0) {
$deletable = $this->backup->executions()->where('status', 'success');
} else {
$deletable = $this->backup->executions()->where('status', 'success')->skip($this->backup->number_of_backups_locally - 1);
}
foreach ($deletable->get() as $execution) {
delete_backup_locally($execution->filename, $this->server);
$execution->delete();
}
}
private function upload_to_s3(): void private function upload_to_s3(): void
{ {
try { try {
@@ -504,12 +495,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else { } else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}"; $commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
} }
if ($this->s3->isHetzner()) { $commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key \"$secret\"";
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
} else {
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
}
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);

View File

@@ -3,9 +3,12 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Server\CleanupDocker; use App\Actions\Server\CleanupDocker;
use App\Events\DockerCleanupDone;
use App\Models\DockerCleanupExecution;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\DockerCleanupFailed; use App\Notifications\Server\DockerCleanupFailed;
use App\Notifications\Server\DockerCleanupSuccess; use App\Notifications\Server\DockerCleanupSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -24,6 +27,8 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public ?string $usageBefore = null; public ?string $usageBefore = null;
public ?DockerCleanupExecution $execution_log = null;
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()]; return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
@@ -38,37 +43,89 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
return; return;
} }
$this->execution_log = DockerCleanupExecution::create([
'server_id' => $this->server->id,
]);
$this->usageBefore = $this->server->getDiskUsage(); $this->usageBefore = $this->server->getDiskUsage();
if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) { if ($this->manualCleanup || $this->server->settings->force_docker_cleanup) {
CleanupDocker::run(server: $this->server); $cleanup_log = CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage(); $usageAfter = $this->server->getDiskUsage();
$this->server->team?->notify(new DockerCleanupSuccess($this->server, ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); $message = ($this->manualCleanup ? 'Manual' : 'Forced').' Docker cleanup job executed successfully. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
return; return;
} }
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) { if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
CleanupDocker::run(server: $this->server); $cleanup_log = CleanupDocker::run(server: $this->server);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk usage could be determined.')); $message = 'Docker cleanup job executed successfully, but no disk usage could be determined.';
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
} }
if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) { if ($this->usageBefore >= $this->server->settings->docker_cleanup_threshold) {
CleanupDocker::run(server: $this->server); $cleanup_log = CleanupDocker::run(server: $this->server);
$usageAfter = $this->server->getDiskUsage(); $usageAfter = $this->server->getDiskUsage();
$diskSaved = $this->usageBefore - $usageAfter; $diskSaved = $this->usageBefore - $usageAfter;
if ($diskSaved > 0) { if ($diskSaved > 0) {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); $message = 'Saved '.$diskSaved.'% disk space. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
} else { } else {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.')); $message = 'Docker cleanup job executed successfully, but no disk space was saved. Disk usage before: '.$this->usageBefore.'%, Disk usage after: '.$usageAfter.'%.';
} }
$this->execution_log->update([
'status' => 'success',
'message' => $message,
'cleanup_log' => $cleanup_log,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
} else { } else {
$this->server->team?->notify(new DockerCleanupSuccess($this->server, 'No cleanup needed for '.$this->server->name)); $message = 'No cleanup needed for '.$this->server->name;
$this->execution_log->update([
'status' => 'success',
'message' => $message,
]);
$this->server->team?->notify(new DockerCleanupSuccess($this->server, $message));
event(new DockerCleanupDone($this->execution_log));
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
if ($this->execution_log) {
$this->execution_log->update([
'status' => 'failed',
'message' => $e->getMessage(),
]);
event(new DockerCleanupDone($this->execution_log));
}
$this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage())); $this->server->team?->notify(new DockerCleanupFailed($this->server, 'Docker cleanup job failed with the following error: '.$e->getMessage()));
throw $e; throw $e;
} finally {
if ($this->execution_log) {
$this->execution_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
} }
} }
} }

View File

@@ -27,19 +27,28 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function handle() public function handle()
{ {
try { try {
$github_access_token = generate_github_jwt_token($this->github_app); $github_access_token = generateGithubJwt($this->github_app);
$response = Http::withHeaders([ $response = Http::withHeaders([
'Authorization' => "Bearer $github_access_token", 'Authorization' => "Bearer $github_access_token",
'Accept' => 'application/vnd.github+json', 'Accept' => 'application/vnd.github+json',
])->get("{$this->github_app->api_url}/app"); ])->get("{$this->github_app->api_url}/app");
if (! $response->successful()) {
throw new \RuntimeException('Failed to fetch GitHub app permissions: '.$response->body());
}
$response = $response->json(); $response = $response->json();
$permissions = data_get($response, 'permissions'); $permissions = data_get($response, 'permissions');
$this->github_app->contents = data_get($permissions, 'contents'); $this->github_app->contents = data_get($permissions, 'contents');
$this->github_app->metadata = data_get($permissions, 'metadata'); $this->github_app->metadata = data_get($permissions, 'metadata');
$this->github_app->pull_requests = data_get($permissions, 'pull_requests'); $this->github_app->pull_requests = data_get($permissions, 'pull_requests');
$this->github_app->administration = data_get($permissions, 'administration'); $this->github_app->administration = data_get($permissions, 'administration');
$this->github_app->save(); $this->github_app->save();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage()); send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage());
throw $e; throw $e;

View File

@@ -25,7 +25,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public function handle(): void public function handle(): void
{ {
try { try {
if (isDev() || isCloud()) { if (isDev()) {
return; return;
} }
$response = Http::retry(3, 1000)->get(config('constants.services.official')); $response = Http::retry(3, 1000)->get(config('constants.services.official'));

View File

@@ -19,6 +19,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; 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\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -68,6 +69,11 @@ class PushServerUpdateJob implements ShouldBeEncrypted, ShouldQueue
public bool $foundLogDrainContainer = false; public bool $foundLogDrainContainer = false;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
}
public function backoff(): int public function backoff(): int
{ {
return isDev() ? 1 : 3; return isDev() ? 1 : 3;

View File

@@ -11,6 +11,7 @@ use App\Models\Service;
use App\Models\Team; use App\Models\Team;
use App\Notifications\ScheduledTask\TaskFailed; use App\Notifications\ScheduledTask\TaskFailed;
use App\Notifications\ScheduledTask\TaskSuccess; use App\Notifications\ScheduledTask\TaskSuccess;
use Carbon\Carbon;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@@ -131,6 +132,11 @@ class ScheduledTaskJob implements ShouldQueue
throw $e; throw $e;
} finally { } finally {
ScheduledTaskDone::dispatch($this->team->id); ScheduledTaskDone::dispatch($this->team->id);
if ($this->task_log) {
$this->task_log->update([
'finished_at' => Carbon::now()->toImmutable(),
]);
}
} }
} }
} }

View File

@@ -24,6 +24,7 @@ class SendMessageToSlackJob implements ShouldQueue
public function handle(): void public function handle(): void
{ {
Http::post($this->webhookUrl, [ Http::post($this->webhookUrl, [
'text' => $this->message->title,
'blocks' => [ 'blocks' => [
[ [
'type' => 'section', 'type' => 'section',

View File

@@ -73,19 +73,21 @@ class StripeProcessJob implements ShouldQueue
} }
$subscription = Subscription::where('team_id', $teamId)->first(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { if ($subscription) {
send_internal_notification('Old subscription activated for team: '.$teamId); // send_internal_notification('Old subscription activated for team: '.$teamId);
$subscription->update([ $subscription->update([
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} else { } else {
send_internal_notification('New subscription for team: '.$teamId); // send_internal_notification('New subscription for team: '.$teamId);
Subscription::create([ Subscription::create([
'team_id' => $teamId, 'team_id' => $teamId,
'stripe_subscription_id' => $subscriptionId, 'stripe_subscription_id' => $subscriptionId,
'stripe_customer_id' => $customerId, 'stripe_customer_id' => $customerId,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} }
break; break;
@@ -100,6 +102,7 @@ class StripeProcessJob implements ShouldQueue
if ($subscription) { if ($subscription) {
$subscription->update([ $subscription->update([
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
'stripe_past_due' => false,
]); ]);
} else { } else {
throw new \RuntimeException("No subscription found for customer: {$customerId}"); throw new \RuntimeException("No subscription found for customer: {$customerId}");
@@ -119,9 +122,7 @@ class StripeProcessJob implements ShouldQueue
} }
if (! $subscription->stripe_invoice_paid) { if (! $subscription->stripe_invoice_paid) {
SubscriptionInvoiceFailedJob::dispatch($team); SubscriptionInvoiceFailedJob::dispatch($team);
send_internal_notification('Invoice payment failed: '.$customerId); // send_internal_notification('Invoice payment failed: '.$customerId);
} else {
send_internal_notification('Invoice payment failed but already paid: '.$customerId);
} }
break; break;
case 'payment_intent.payment_failed': case 'payment_intent.payment_failed':
@@ -136,7 +137,7 @@ class StripeProcessJob implements ShouldQueue
return; return;
} }
send_internal_notification('Subscription payment failed for customer: '.$customerId); // send_internal_notification('Subscription payment failed for customer: '.$customerId);
break; break;
case 'customer.subscription.created': case 'customer.subscription.created':
$customerId = data_get($data, 'customer'); $customerId = data_get($data, 'customer');
@@ -158,7 +159,7 @@ class StripeProcessJob implements ShouldQueue
} }
$subscription = Subscription::where('team_id', $teamId)->first(); $subscription = Subscription::where('team_id', $teamId)->first();
if ($subscription) { if ($subscription) {
send_internal_notification("Subscription already exists for team: {$teamId}"); // send_internal_notification("Subscription already exists for team: {$teamId}");
throw new \RuntimeException("Subscription already exists for team: {$teamId}"); throw new \RuntimeException("Subscription already exists for team: {$teamId}");
} else { } else {
Subscription::create([ Subscription::create([
@@ -182,7 +183,7 @@ class StripeProcessJob implements ShouldQueue
$subscription = Subscription::where('stripe_customer_id', $customerId)->first(); $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) { if (! $subscription) {
if ($status === 'incomplete_expired') { if ($status === 'incomplete_expired') {
send_internal_notification('Subscription incomplete expired'); // send_internal_notification('Subscription incomplete expired');
throw new \RuntimeException('Subscription incomplete expired'); throw new \RuntimeException('Subscription incomplete expired');
} }
if ($teamId) { if ($teamId) {
@@ -224,9 +225,33 @@ class StripeProcessJob implements ShouldQueue
]); ]);
} }
} }
if ($status === 'past_due') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_past_due' => true,
]);
send_internal_notification('Past Due: '.$customerId.'Subscription ID: '.$subscriptionId);
}
}
if ($status === 'unpaid') {
if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([
'stripe_invoice_paid' => false,
]);
send_internal_notification('Unpaid: '.$customerId.'Subscription ID: '.$subscriptionId);
}
$team = data_get($subscription, 'team');
if ($team) {
$team->subscriptionEnded();
} else {
send_internal_notification('Subscription unpaid but no team found in Coolify for customer: '.$customerId);
throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
}
}
if ($status === 'active') { if ($status === 'active') {
if ($subscription->stripe_subscription_id === $subscriptionId) { if ($subscription->stripe_subscription_id === $subscriptionId) {
$subscription->update([ $subscription->update([
'stripe_past_due' => false,
'stripe_invoice_paid' => true, 'stripe_invoice_paid' => true,
]); ]);
} }

104
app/Jobs/VolumeCloneJob.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
namespace App\Jobs;
use App\Models\LocalPersistentVolume;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class VolumeCloneJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected string $cloneDir = '/data/coolify/clone';
public function __construct(
protected string $sourceVolume,
protected string $targetVolume,
protected Server $sourceServer,
protected ?Server $targetServer,
protected LocalPersistentVolume $persistentVolume
) {
$this->onQueue('high');
}
public function handle()
{
try {
if (! $this->targetServer || $this->targetServer->id === $this->sourceServer->id) {
$this->cloneLocalVolume();
} else {
$this->cloneRemoteVolume();
}
} catch (\Exception $e) {
\Log::error("Failed to copy volume data for {$this->sourceVolume}: ".$e->getMessage());
throw $e;
}
}
protected function cloneLocalVolume()
{
instant_remote_process([
"docker volume create $this->targetVolume",
"docker run --rm -v $this->sourceVolume:/source -v $this->targetVolume:/target alpine sh -c 'cp -a /source/. /target/ && chown -R 1000:1000 /target'",
], $this->sourceServer);
}
protected function cloneRemoteVolume()
{
$sourceCloneDir = "{$this->cloneDir}/{$this->sourceVolume}";
$targetCloneDir = "{$this->cloneDir}/{$this->targetVolume}";
try {
instant_remote_process([
"mkdir -p $sourceCloneDir",
"chmod 777 $sourceCloneDir",
"docker run --rm -v $this->sourceVolume:/source -v $sourceCloneDir:/clone alpine sh -c 'cd /source && tar czf /clone/volume-data.tar.gz .'",
], $this->sourceServer);
instant_remote_process([
"mkdir -p $targetCloneDir",
"chmod 777 $targetCloneDir",
], $this->targetServer);
instant_scp(
"$sourceCloneDir/volume-data.tar.gz",
"$targetCloneDir/volume-data.tar.gz",
$this->sourceServer,
$this->targetServer
);
instant_remote_process([
"docker volume create $this->targetVolume",
"docker run --rm -v $this->targetVolume:/target -v $targetCloneDir:/clone alpine sh -c 'cd /target && tar xzf /clone/volume-data.tar.gz && chown -R 1000:1000 /target'",
], $this->targetServer);
} catch (\Exception $e) {
\Log::error("Failed to clone volume {$this->sourceVolume} to {$this->targetVolume}: ".$e->getMessage());
throw $e;
} finally {
try {
instant_remote_process([
"rm -rf $sourceCloneDir",
], $this->sourceServer, false);
} catch (\Exception $e) {
\Log::warning('Failed to clean up source server clone directory: '.$e->getMessage());
}
try {
if ($this->targetServer) {
instant_remote_process([
"rm -rf $targetCloneDir",
], $this->targetServer, false);
}
} catch (\Exception $e) {
\Log::warning('Failed to clean up target server clone directory: '.$e->getMessage());
}
}
}
}

View File

@@ -42,14 +42,8 @@ class ActivityMonitor extends Component
public function polling() public function polling()
{ {
$this->hydrateActivity(); $this->hydrateActivity();
// $this->setStatus(ProcessStatus::IN_PROGRESS);
$exit_code = data_get($this->activity, 'properties.exitCode'); $exit_code = data_get($this->activity, 'properties.exitCode');
if ($exit_code !== null) { if ($exit_code !== null) {
// if ($exit_code === 0) {
// // $this->setStatus(ProcessStatus::FINISHED);
// } else {
// // $this->setStatus(ProcessStatus::ERROR);
// }
$this->isPollingActive = false; $this->isPollingActive = false;
if ($exit_code === 0) { if ($exit_code === 0) {
if ($this->eventToDispatch !== null) { if ($this->eventToDispatch !== null) {
@@ -70,12 +64,4 @@ class ActivityMonitor extends Component
} }
} }
} }
// protected function setStatus($status)
// {
// $this->activity->properties = $this->activity->properties->merge([
// 'status' => $status,
// ]);
// $this->activity->save();
// }
} }

View File

@@ -21,16 +21,28 @@ class Index extends Component
public function mount() public function mount()
{ {
if (! isCloud()) { if (! isCloud() && ! isDev()) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (Auth::id() !== 0 && ! session('impersonating')) {
if (Auth::id() !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$this->getSubscribers(); $this->getSubscribers();
} }
public function back()
{
if (session('impersonating')) {
session()->forget('impersonating');
$user = User::find(0);
$team_to_switch_to = $user->teams->first();
Auth::login($user);
refreshSession($team_to_switch_to);
return redirect(request()->header('Referer'));
}
}
public function submitSearch() public function submitSearch()
{ {
if ($this->search !== '') { if ($this->search !== '') {
@@ -52,9 +64,10 @@ class Index extends Component
if (Auth::id() !== 0) { if (Auth::id() !== 0) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
session(['impersonating' => true]);
$user = User::find($user_id); $user = User::find($user_id);
$team_to_switch_to = $user->teams->first(); $team_to_switch_to = $user->teams->first();
Cache::forget("team:{$user->id}"); // Cache::forget("team:{$user->id}");
Auth::login($user); Auth::login($user);
refreshSession($team_to_switch_to); refreshSession($team_to_switch_to);

View File

@@ -9,6 +9,7 @@ use App\Models\Server;
use App\Models\Team; use App\Models\Team;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class Index extends Component class Index extends Component
{ {
@@ -334,6 +335,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdProject = Project::create([ $this->createdProject = Project::create([
'name' => 'My first project', 'name' => 'My first project',
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]); ]);
$this->currentState = 'create-resource'; $this->currentState = 'create-resource';
} }
@@ -346,7 +348,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'project.resource.create', 'project.resource.create',
[ [
'project_uuid' => $this->createdProject->uuid, 'project_uuid' => $this->createdProject->uuid,
'environment_name' => 'production', 'environment_uuid' => $this->createdProject->environments->first()->uuid,
'server' => $this->createdServer->id, 'server' => $this->createdServer->id,
] ]
); );

View File

@@ -49,6 +49,11 @@ class Dashboard extends Component
])->sortBy('id')->groupBy('server_name')->toArray(); ])->sortBy('id')->groupBy('server_name')->toArray();
} }
public function navigateToProject($projectUuid)
{
return $this->redirect(collect($this->projects)->firstWhere('uuid', $projectUuid)->navigateTo(), true);
}
public function render() public function render()
{ {
return view('livewire.dashboard'); return view('livewire.dashboard');

View File

@@ -35,10 +35,18 @@ class Docker extends Component
$this->network = new Cuid2; $this->network = new Cuid2;
$this->servers = Server::isUsable()->get(); $this->servers = Server::isUsable()->get();
if ($server_id) { if ($server_id) {
$this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first(); $foundServer = $this->servers->find($server_id) ?: $this->servers->first();
if (! $foundServer) {
throw new \Exception('Server not found.');
}
$this->selectedServer = $foundServer;
$this->serverId = $this->selectedServer->id; $this->serverId = $this->selectedServer->id;
} else { } else {
$this->selectedServer = $this->servers->first(); $foundServer = $this->servers->first();
if (! $foundServer) {
throw new \Exception('Server not found.');
}
$this->selectedServer = $foundServer;
$this->serverId = $this->selectedServer->id; $this->serverId = $this->selectedServer->id;
} }
$this->generateName(); $this->generateName();
@@ -83,9 +91,7 @@ class Docker extends Component
]); ]);
} }
} }
$connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer); $this->redirect(route('destination.show', $docker->uuid));
instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false);
$this->dispatch('reloadWindow');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -36,7 +36,7 @@ class Help extends Component
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
// Sending feedback through Cloud API // Sending feedback through Cloud API
if ($type === false) { if (blank($type)) {
$url = 'https://app.coolify.io/api/feedback'; $url = 'https://app.coolify.io/api/feedback';
Http::post($url, [ Http::post($url, [
'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`',

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project;
use App\Models\Project; use App\Models\Project;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class AddEmpty extends Component class AddEmpty extends Component
{ {
@@ -22,6 +23,7 @@ class AddEmpty extends Component
'name' => $this->name, 'name' => $this->name,
'description' => $this->description, 'description' => $this->description,
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]); ]);
return redirect()->route('project.show', $project->uuid); return redirect()->route('project.show', $project->uuid);

View File

@@ -124,9 +124,20 @@ class Advanced extends Component
} }
} }
private function resetDefaultLabels()
{
if ($this->application->settings->is_container_label_readonly_enabled === false) {
return;
}
$customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($customLabels);
$this->application->save();
}
public function instantSave() public function instantSave()
{ {
try { try {
$reset = false;
if ($this->isLogDrainEnabled) { if ($this->isLogDrainEnabled) {
if (! $this->application->destination->server->isLogDrainEnabled()) { if (! $this->application->destination->server->isLogDrainEnabled()) {
$this->isLogDrainEnabled = false; $this->isLogDrainEnabled = false;
@@ -140,7 +151,7 @@ class Advanced extends Component
$this->application->isGzipEnabled() !== $this->isGzipEnabled || $this->application->isGzipEnabled() !== $this->isGzipEnabled ||
$this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled
) { ) {
$this->dispatch('resetDefaultLabels', false); $reset = true;
} }
if ($this->application->settings->is_raw_compose_deployment_enabled) { if ($this->application->settings->is_raw_compose_deployment_enabled) {
@@ -149,6 +160,11 @@ class Advanced extends Component
$this->application->parse(); $this->application->parse();
} }
$this->syncData(true); $this->syncData(true);
if ($reset) {
$this->resetDefaultLabels();
}
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -3,43 +3,42 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Models\Application; use App\Models\Application;
use App\Models\Server;
use Livewire\Component; use Livewire\Component;
class Configuration extends Component class Configuration extends Component
{ {
public $currentRoute;
public Application $application; public Application $application;
public $project;
public $environment;
public $servers; public $servers;
protected $listeners = ['buildPackUpdated' => '$refresh']; protected $listeners = ['buildPackUpdated' => '$refresh'];
public function mount() public function mount()
{ {
$this->currentRoute = request()->route()->getName();
$project = currentTeam() $project = currentTeam()
->projects() ->projects()
->select('id', 'uuid', 'team_id') ->select('id', 'uuid', 'team_id')
->where('uuid', request()->route('project_uuid')) ->where('uuid', request()->route('project_uuid'))
->firstOrFail(); ->firstOrFail();
$environment = $project->environments() $environment = $project->environments()
->select('id', 'name', 'project_id') ->select('id', 'uuid', 'name', 'project_id')
->where('name', request()->route('environment_name')) ->where('uuid', request()->route('environment_uuid'))
->firstOrFail(); ->firstOrFail();
$application = $environment->applications() $application = $environment->applications()
->with(['destination']) ->with(['destination'])
->where('uuid', request()->route('application_uuid')) ->where('uuid', request()->route('application_uuid'))
->firstOrFail(); ->firstOrFail();
$this->project = $project;
$this->environment = $environment;
$this->application = $application; $this->application = $application;
if ($application->destination && $application->destination->server) {
$mainServer = $application->destination->server;
$this->servers = Server::ownedByCurrentTeam()
->select('id', 'name')
->where('id', '!=', $mainServer->id)
->get();
} else {
$this->servers = collect();
}
} }
public function render() public function render()

View File

@@ -18,7 +18,7 @@ class Index extends Component
public int $skip = 0; public int $skip = 0;
public int $default_take = 40; public int $default_take = 10;
public bool $show_next = false; public bool $show_next = false;
@@ -34,7 +34,7 @@ class Index extends Component
if (! $project) { if (! $project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) { if (! $environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -42,7 +42,7 @@ class Index extends Component
if (! $application) { if (! $application) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
['deployments' => $deployments, 'count' => $count] = $application->deployments(0, 40); ['deployments' => $deployments, 'count' => $count] = $application->deployments(0, $this->default_take);
$this->application = $application; $this->application = $application;
$this->deployments = $deployments; $this->deployments = $deployments;
$this->deployments_count = $count; $this->deployments_count = $count;

View File

@@ -14,6 +14,8 @@ class Show extends Component
public string $deployment_uuid; public string $deployment_uuid;
public string $horizon_job_status;
public $isKeepAliveOn = true; public $isKeepAliveOn = true;
protected $listeners = ['refreshQueue']; protected $listeners = ['refreshQueue'];
@@ -26,7 +28,7 @@ class Show extends Component
if (! $project) { if (! $project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) { if (! $environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -34,25 +36,19 @@ class Show extends Component
if (! $application) { if (! $application) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
// $activity = Activity::where('properties->type_uuid', '=', $deploymentUuid)->first();
// if (!$activity) {
// return redirect()->route('project.application.deployment.index', [
// 'project_uuid' => $project->uuid,
// 'environment_name' => $environment->name,
// 'application_uuid' => $application->uuid,
// ]);
// }
$application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first(); $application_deployment_queue = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
if (! $application_deployment_queue) { if (! $application_deployment_queue) {
return redirect()->route('project.application.deployment.index', [ return redirect()->route('project.application.deployment.index', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
]); ]);
} }
$this->application = $application; $this->application = $application;
$this->application_deployment_queue = $application_deployment_queue; $this->application_deployment_queue = $application_deployment_queue;
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->deployment_uuid = $deploymentUuid; $this->deployment_uuid = $deploymentUuid;
$this->isKeepAliveOn();
} }
public function refreshQueue() public function refreshQueue()
@@ -60,13 +56,21 @@ class Show extends Component
$this->application_deployment_queue->refresh(); $this->application_deployment_queue->refresh();
} }
private function isKeepAliveOn()
{
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') {
$this->isKeepAliveOn = false;
} else {
$this->isKeepAliveOn = true;
}
}
public function polling() public function polling()
{ {
$this->dispatch('deploymentFinished'); $this->dispatch('deploymentFinished');
$this->application_deployment_queue->refresh(); $this->application_deployment_queue->refresh();
if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') { $this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
$this->isKeepAliveOn = false; $this->isKeepAliveOn();
}
} }
public function getLogLinesProperty() public function getLogLinesProperty()

View File

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

View File

@@ -155,7 +155,7 @@ class General extends Component
$this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled; $this->is_preserve_repository_enabled = $this->application->settings->is_preserve_repository_enabled;
$this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled; $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels(); $this->customLabels = $this->application->parseContainerLabels();
if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && ! $this->application->settings->is_container_label_readonly_enabled) { if (! $this->customLabels && $this->application->destination->server->proxyType() !== 'NONE' && $this->application->settings->is_container_label_readonly_enabled === true) {
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
$this->application->custom_labels = base64_encode($this->customLabels); $this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save(); $this->application->save();
@@ -189,6 +189,9 @@ class General extends Component
}); });
} }
} }
if ($this->application->settings->is_container_label_readonly_enabled) {
$this->resetDefaultLabels(false);
}
} }
public function loadComposeFile($isInit = false) public function loadComposeFile($isInit = false)
@@ -296,7 +299,7 @@ class General extends Component
public function resetDefaultLabels($manualReset = false) public function resetDefaultLabels($manualReset = false)
{ {
try { try {
if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { if (! $this->application->settings->is_container_label_readonly_enabled && ! $manualReset) {
return; return;
} }
$this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n");
@@ -326,10 +329,11 @@ class General extends Component
} }
check_domain_usage(resource: $this->application); check_domain_usage(resource: $this->application);
$this->application->fqdn = $domains->implode(','); $this->application->fqdn = $domains->implode(',');
$this->resetDefaultLabels(false);
} }
} }
public function set_redirect() public function setRedirect()
{ {
try { try {
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
@@ -362,10 +366,10 @@ class General extends Component
if ($warning) { if ($warning) {
$this->dispatch('warning', __('warning.sslipdomain')); $this->dispatch('warning', __('warning.sslipdomain'));
} }
$this->resetDefaultLabels(); // $this->resetDefaultLabels();
if ($this->application->isDirty('redirect')) { if ($this->application->isDirty('redirect')) {
$this->set_redirect(); $this->setRedirect();
} }
$this->checkFqdns(); $this->checkFqdns();
@@ -444,6 +448,7 @@ class General extends Component
{ {
$config = GenerateConfig::run($this->application, true); $config = GenerateConfig::run($this->application, true);
$fileName = str($this->application->name)->slug()->append('_config.json'); $fileName = str($this->application->name)->slug()->append('_config.json');
dd($config);
return response()->streamDownload(function () use ($config) { return response()->streamDownload(function () use ($config) {
echo $config; echo $config;

View File

@@ -38,7 +38,7 @@ class Heading extends Component
{ {
$this->parameters = [ $this->parameters = [
'project_uuid' => $this->application->project()->uuid, 'project_uuid' => $this->application->project()->uuid,
'environment_name' => $this->application->environment->name, 'environment_uuid' => $this->application->environment->uuid,
'application_uuid' => $this->application->uuid, 'application_uuid' => $this->application->uuid,
]; ];
$lastDeployment = $this->application->get_last_successful_deployment(); $lastDeployment = $this->application->get_last_successful_deployment();
@@ -90,12 +90,12 @@ class Heading extends Component
force_rebuild: $force_rebuild, force_rebuild: $force_rebuild,
); );
return redirect()->route('project.application.deployment.show', [ return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
]); ], navigate: true);
} }
protected function setDeploymentUuid() protected function setDeploymentUuid()
@@ -132,12 +132,12 @@ class Heading extends Component
restart_only: true, restart_only: true,
); );
return redirect()->route('project.application.deployment.show', [ return $this->redirectRoute('project.application.deployment.show', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deploymentUuid, 'deployment_uuid' => $this->deploymentUuid,
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
]); ], navigate: true);
} }
public function render() public function render()

View File

@@ -171,7 +171,7 @@ class Previews extends Component
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $this->deployment_uuid, 'deployment_uuid' => $this->deployment_uuid,
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -37,7 +37,7 @@ class Rollback extends Component
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'application_uuid' => $this->parameters['application_uuid'], 'application_uuid' => $this->parameters['application_uuid'],
'deployment_uuid' => $deployment_uuid, 'deployment_uuid' => $deployment_uuid,
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
]); ]);
} }

View File

@@ -2,6 +2,12 @@
namespace App\Livewire\Project; namespace App\Livewire\Project;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StartDatabase;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Jobs\VolumeCloneJob;
use App\Models\Environment; use App\Models\Environment;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
@@ -12,7 +18,7 @@ class CloneMe extends Component
{ {
public string $project_uuid; public string $project_uuid;
public string $environment_name; public string $environment_uuid;
public int $project_id; public int $project_id;
@@ -34,6 +40,8 @@ class CloneMe extends Component
public string $newName = ''; public string $newName = '';
public bool $cloneVolumeData = false;
protected $messages = [ protected $messages = [
'selectedServer' => 'Please select a server.', 'selectedServer' => 'Please select a server.',
'selectedDestination' => 'Please select a server & destination.', 'selectedDestination' => 'Please select a server & destination.',
@@ -44,12 +52,17 @@ class CloneMe extends Component
{ {
$this->project_uuid = $project_uuid; $this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail(); $this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('name', $this->environment_name)->first(); $this->environment = $this->project->environments->where('uuid', $this->environment_uuid)->first();
$this->project_id = $this->project->id; $this->project_id = $this->project->id;
$this->servers = currentTeam()->servers; $this->servers = currentTeam()->servers;
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug(); $this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
} }
public function toggleVolumeCloning(bool $value)
{
$this->cloneVolumeData = $value;
}
public function render() public function render()
{ {
return view('livewire.project.clone-me'); return view('livewire.project.clone-me');
@@ -89,6 +102,7 @@ class CloneMe extends Component
if ($this->environment->name !== 'production') { if ($this->environment->name !== 'production') {
$project->environments()->create([ $project->environments()->create([
'name' => $this->environment->name, 'name' => $this->environment->name,
'uuid' => (string) new Cuid2,
]); ]);
} }
$environment = $project->environments->where('name', $this->environment->name)->first(); $environment = $project->environments->where('name', $this->environment->name)->first();
@@ -100,41 +114,160 @@ class CloneMe extends Component
$project = $this->project; $project = $this->project;
$environment = $this->project->environments()->create([ $environment = $this->project->environments()->create([
'name' => $this->newName, 'name' => $this->newName,
'uuid' => (string) new Cuid2,
]); ]);
} }
$applications = $this->environment->applications; $applications = $this->environment->applications;
$databases = $this->environment->databases(); $databases = $this->environment->databases();
$services = $this->environment->services; $services = $this->environment->services;
foreach ($applications as $application) { foreach ($applications as $application) {
$applicationSettings = $application->settings;
$uuid = (string) new Cuid2; $uuid = (string) new Cuid2;
$newApplication = $application->replicate()->fill([ $url = $application->fqdn;
if ($this->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$url = generateFqdn($this->server, $uuid);
}
$newApplication = $application->replicate([
'id',
'created_at',
'updated_at',
'additional_servers_count',
'additional_networks_count',
])->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid), 'fqdn' => $url,
'status' => 'exited', 'status' => 'exited',
'environment_id' => $environment->id, 'environment_id' => $environment->id,
// This is not correct, but we need to set it to something
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newApplication->save(); $newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { if ($newApplication->destination->server->proxyType() !== 'NONE' && $applicationSettings->is_container_label_readonly_enabled === true) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([ $customLabels = str(implode('|coolify|', generateLabelsApplication($newApplication)))->replace('|coolify|', "\n");
$newApplication->custom_labels = base64_encode($customLabels);
$newApplication->save();
}
$newApplication->settings()->delete();
if ($applicationSettings) {
$newApplicationSettings = $applicationSettings->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $newApplication->id, 'application_id' => $newApplication->id,
]); ]);
$newEnvironmentVariable->save(); $newApplicationSettings->save();
} }
$tags = $application->tags;
foreach ($tags as $tag) {
$newApplication->tags()->attach($tag->id);
}
$scheduledTasks = $application->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'application_id' => $newApplication->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$applicationPreviews = $application->previews()->get();
foreach ($applicationPreviews as $preview) {
$newPreview = $preview->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'application_id' => $newApplication->id,
'status' => 'exited',
]);
$newPreview->save();
}
$persistentVolumes = $application->persistentStorages()->get(); $persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) { foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([ $newName = '';
'name' => $newApplication->uuid.'-'.str($volume->name)->afterLast('-'), if (str_starts_with($volume->name, $application->uuid)) {
$newName = str($volume->name)->replace($application->uuid, $newApplication->uuid);
} else {
$newName = $newApplication->uuid.'-'.$volume->name;
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $newApplication->id, 'resource_id' => $newApplication->id,
]); ]);
$newPersistentVolume->save(); $newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopApplication::dispatch($application, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->destination->server;
$targetServer = $newApplication->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
queue_application_deployment(
deployment_uuid: (string) new Cuid2,
application: $application,
server: $sourceServer,
destination: $application->destination,
no_questions_asked: true
);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
} }
} }
}
$fileStorages = $application->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $newApplication->id,
]);
$newStorage->save();
}
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
}
foreach ($databases as $database) { foreach ($databases as $database) {
$uuid = (string) new Cuid2; $uuid = (string) new Cuid2;
$newDatabase = $database->replicate()->fill([ $newDatabase = $database->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'status' => 'exited', 'status' => 'exited',
'started_at' => null, 'started_at' => null,
@@ -142,51 +275,294 @@ class CloneMe extends Component
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newDatabase->save(); $newDatabase->save();
$tags = $database->tags;
foreach ($tags as $tag) {
$newDatabase->tags()->attach($tag->id);
}
$newDatabase->persistentStorages()->delete();
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$originalName = $volume->name;
$newName = '';
if (str_starts_with($originalName, 'postgres-data-')) {
$newName = 'postgres-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mysql-data-')) {
$newName = 'mysql-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'redis-data-')) {
$newName = 'redis-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'clickhouse-data-')) {
$newName = 'clickhouse-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mariadb-data-')) {
$newName = 'mariadb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'mongodb-data-')) {
$newName = 'mongodb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'keydb-data-')) {
$newName = 'keydb-data-'.$newDatabase->uuid;
} elseif (str_starts_with($originalName, 'dragonfly-data-')) {
$newName = 'dragonfly-data-'.$newDatabase->uuid;
} else {
if (str_starts_with($volume->name, $database->uuid)) {
$newName = str($volume->name)->replace($database->uuid, $newDatabase->uuid);
} else {
$newName = $newDatabase->uuid.'-'.$volume->name;
}
}
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $newDatabase->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopDatabase::dispatch($database);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->destination->server;
$targetServer = $newDatabase->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartDatabase::dispatch($database);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $database->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $newDatabase->id,
]);
$newStorage->save();
}
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $newDatabase->id,
'database_type' => $newDatabase->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
$environmentVaribles = $database->environment_variables()->get(); $environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) { foreach ($environmentVaribles as $environmentVarible) {
$payload = []; $payload = [];
if ($database->type() === 'standalone-postgresql') { $payload['resourceable_id'] = $newDatabase->id;
$payload['standalone_postgresql_id'] = $newDatabase->id; $payload['resourceable_type'] = $newDatabase->getMorphClass();
} elseif ($database->type() === 'standalone-redis') { $newEnvironmentVariable = $environmentVarible->replicate([
$payload['standalone_redis_id'] = $newDatabase->id; 'id',
} elseif ($database->type() === 'standalone-mongodb') { 'created_at',
$payload['standalone_mongodb_id'] = $newDatabase->id; 'updated_at',
} elseif ($database->type() === 'standalone-mysql') { ])->fill($payload);
$payload['standalone_mysql_id'] = $newDatabase->id;
} elseif ($database->type() === 'standalone-mariadb') {
$payload['standalone_mariadb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save(); $newEnvironmentVariable->save();
} }
} }
foreach ($services as $service) { foreach ($services as $service) {
$uuid = (string) new Cuid2; $uuid = (string) new Cuid2;
$newService = $service->replicate()->fill([ $newService = $service->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid, 'uuid' => $uuid,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => $this->selectedDestination, 'destination_id' => $this->selectedDestination,
]); ]);
$newService->save(); $newService->save();
$tags = $service->tags;
foreach ($tags as $tag) {
$newService->tags()->attach($tag->id);
}
$scheduledTasks = $service->scheduled_tasks()->get();
foreach ($scheduledTasks as $task) {
$newTask = $task->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => (string) new Cuid2,
'service_id' => $newService->id,
'team_id' => currentTeam()->id,
]);
$newTask->save();
}
$environmentVariables = $service->environment_variables()->get();
foreach ($environmentVariables as $environmentVariable) {
$newEnvironmentVariable = $environmentVariable->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resourceable_id' => $newService->id,
'resourceable_type' => $newService->getMorphClass(),
]);
$newEnvironmentVariable->save();
}
foreach ($newService->applications() as $application) { foreach ($newService->applications() as $application) {
$application->update([ $application->update([
'status' => 'exited', 'status' => 'exited',
]); ]);
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $application->uuid)) {
$newName = str($volume->name)->replace($application->uuid, $application->uuid);
} else {
$newName = $application->uuid.'-'.$volume->name;
} }
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $application->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($application, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $application->service->destination->server;
$targetServer = $newService->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($application);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $application->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $application->id,
]);
$newStorage->save();
}
}
foreach ($newService->databases() as $database) { foreach ($newService->databases() as $database) {
$database->update([ $database->update([
'status' => 'exited', 'status' => 'exited',
]); ]);
$persistentVolumes = $database->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newName = '';
if (str_starts_with($volume->name, $database->uuid)) {
$newName = str($volume->name)->replace($database->uuid, $database->uuid);
} else {
$newName = $database->uuid.'-'.$volume->name;
} }
$newPersistentVolume = $volume->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'name' => $newName,
'resource_id' => $database->id,
]);
$newPersistentVolume->save();
if ($this->cloneVolumeData) {
try {
StopService::dispatch($database->service, false, false);
$sourceVolume = $volume->name;
$targetVolume = $newPersistentVolume->name;
$sourceServer = $database->service->destination->server;
$targetServer = $newService->destination->server;
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
StartService::dispatch($database->service);
} catch (\Exception $e) {
\Log::error('Failed to copy volume data for '.$volume->name.': '.$e->getMessage());
}
}
}
$fileStorages = $database->fileStorages()->get();
foreach ($fileStorages as $storage) {
$newStorage = $storage->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'resource_id' => $database->id,
]);
$newStorage->save();
}
$scheduledBackups = $database->scheduledBackups()->get();
foreach ($scheduledBackups as $backup) {
$uuid = (string) new Cuid2;
$newBackup = $backup->replicate([
'id',
'created_at',
'updated_at',
])->fill([
'uuid' => $uuid,
'database_id' => $database->id,
'database_type' => $database->getMorphClass(),
'team_id' => currentTeam()->id,
]);
$newBackup->save();
}
}
$newService->parse(); $newService->parse();
} }
} catch (\Exception $e) {
handleError($e, $this);
return;
} finally {
if (! isset($e)) {
return redirect()->route('project.resource.index', [ return redirect()->route('project.resource.index', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
]); ]);
} catch (\Exception $e) { }
return handleError($e, $this);
} }
} }
} }

View File

@@ -22,7 +22,7 @@ class Execution extends Component
if (! $project) { if (! $project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) { if (! $environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }

View File

@@ -14,7 +14,7 @@ class Index extends Component
if (! $project) { if (! $project) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); $environment = $project->load(['environments'])->environments->where('uuid', request()->route('environment_uuid'))->first()->load(['applications']);
if (! $environment) { if (! $environment) {
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -31,7 +31,7 @@ class Index extends Component
) { ) {
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'database_uuid' => $database->uuid, 'database_uuid' => $database->uuid,
]); ]);
} }

View File

@@ -40,8 +40,26 @@ class BackupEdit extends Component
#[Validate(['required', 'string'])] #[Validate(['required', 'string'])]
public string $frequency = ''; public string $frequency = '';
#[Validate(['required', 'integer', 'min:1'])] #[Validate(['string'])]
public int $numberOfBackupsLocally = 1; public string $timezone = '';
#[Validate(['required', 'integer'])]
public int $databaseBackupRetentionAmountLocally = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionDaysLocally = 0;
#[Validate(['required', 'numeric', 'min:0'])]
public ?float $databaseBackupRetentionMaxStorageLocally = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionAmountS3 = 0;
#[Validate(['required', 'integer'])]
public ?int $databaseBackupRetentionDaysS3 = 0;
#[Validate(['required', 'numeric', 'min:0'])]
public ?float $databaseBackupRetentionMaxStorageS3 = 0;
#[Validate(['required', 'boolean'])] #[Validate(['required', 'boolean'])]
public bool $saveS3 = false; public bool $saveS3 = false;
@@ -68,19 +86,30 @@ class BackupEdit extends Component
public function syncData(bool $toModel = false) public function syncData(bool $toModel = false)
{ {
if ($toModel) { if ($toModel) {
$this->customValidate();
$this->backup->enabled = $this->backupEnabled; $this->backup->enabled = $this->backupEnabled;
$this->backup->frequency = $this->frequency; $this->backup->frequency = $this->frequency;
$this->backup->number_of_backups_locally = $this->numberOfBackupsLocally; $this->backup->database_backup_retention_amount_locally = $this->databaseBackupRetentionAmountLocally;
$this->backup->database_backup_retention_days_locally = $this->databaseBackupRetentionDaysLocally;
$this->backup->database_backup_retention_max_storage_locally = $this->databaseBackupRetentionMaxStorageLocally;
$this->backup->database_backup_retention_amount_s3 = $this->databaseBackupRetentionAmountS3;
$this->backup->database_backup_retention_days_s3 = $this->databaseBackupRetentionDaysS3;
$this->backup->database_backup_retention_max_storage_s3 = $this->databaseBackupRetentionMaxStorageS3;
$this->backup->save_s3 = $this->saveS3; $this->backup->save_s3 = $this->saveS3;
$this->backup->s3_storage_id = $this->s3StorageId; $this->backup->s3_storage_id = $this->s3StorageId;
$this->backup->databases_to_backup = $this->databasesToBackup; $this->backup->databases_to_backup = $this->databasesToBackup;
$this->backup->dump_all = $this->dumpAll; $this->backup->dump_all = $this->dumpAll;
$this->customValidate();
$this->backup->save(); $this->backup->save();
} else { } else {
$this->backupEnabled = $this->backup->enabled; $this->backupEnabled = $this->backup->enabled;
$this->frequency = $this->backup->frequency; $this->frequency = $this->backup->frequency;
$this->numberOfBackupsLocally = $this->backup->number_of_backups_locally; $this->timezone = data_get($this->backup->server(), 'settings.server_timezone', 'Instance timezone');
$this->databaseBackupRetentionAmountLocally = $this->backup->database_backup_retention_amount_locally;
$this->databaseBackupRetentionDaysLocally = $this->backup->database_backup_retention_days_locally;
$this->databaseBackupRetentionMaxStorageLocally = $this->backup->database_backup_retention_max_storage_locally;
$this->databaseBackupRetentionAmountS3 = $this->backup->database_backup_retention_amount_s3;
$this->databaseBackupRetentionDaysS3 = $this->backup->database_backup_retention_days_s3;
$this->databaseBackupRetentionMaxStorageS3 = $this->backup->database_backup_retention_max_storage_s3;
$this->saveS3 = $this->backup->save_s3; $this->saveS3 = $this->backup->save_s3;
$this->s3StorageId = $this->backup->s3_storage_id; $this->s3StorageId = $this->backup->s3_storage_id;
$this->databasesToBackup = $this->backup->databases_to_backup; $this->databasesToBackup = $this->backup->databases_to_backup;
@@ -99,11 +128,29 @@ class BackupEdit extends Component
} }
try { try {
if ($this->delete_associated_backups_locally) { $server = null;
$this->deleteAssociatedBackupsLocally(); if ($this->backup->database instanceof \App\Models\ServiceDatabase) {
$server = $this->backup->database->service->destination->server;
} elseif ($this->backup->database->destination && $this->backup->database->destination->server) {
$server = $this->backup->database->destination->server;
}
$filenames = $this->backup->executions()
->whereNotNull('filename')
->where('filename', '!=', '')
->where('scheduled_database_backup_id', $this->backup->id)
->pluck('filename')
->filter()
->all();
if (! empty($filenames)) {
if ($this->delete_associated_backups_locally && $server) {
deleteBackupsLocally($filenames, $server);
}
if ($this->delete_associated_backups_s3 && $this->backup->s3) {
deleteBackupsS3($filenames, $this->backup->s3);
} }
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
} }
$this->backup->delete(); $this->backup->delete();
@@ -119,7 +166,9 @@ class BackupEdit extends Component
} else { } else {
return redirect()->route('project.database.backup.index', $this->parameters); return redirect()->route('project.database.backup.index', $this->parameters);
} }
} catch (\Throwable $e) { } catch (\Exception $e) {
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
return handleError($e, $this); return handleError($e, $this);
} }
} }
@@ -156,63 +205,12 @@ class BackupEdit extends Component
} }
} }
private function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (! $backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if (str($backupFolder)->isNotEmpty()) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
private function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
private function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render() public function render()
{ {
return view('livewire.project.database.backup-edit', [ return view('livewire.project.database.backup-edit', [
'checkboxes' => [ 'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')], ['id' => 'delete_associated_backups_locally', 'label' => __('database.delete_backups_locally')],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.'] ['id' => 'delete_associated_backups_s3', 'label' => 'All backups will be permanently deleted (associated with this backup job) from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.'] // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
], ],
]); ]);

View File

@@ -18,9 +18,9 @@ class BackupExecutions extends Component
public $setDeletableBackup; public $setDeletableBackup;
public $delete_backup_s3 = true; public $delete_backup_s3 = false;
public $delete_backup_sftp = true; public $delete_backup_sftp = false;
public function getListeners() public function getListeners()
{ {
@@ -57,23 +57,25 @@ class BackupExecutions extends Component
return; return;
} }
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $server = $execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); ? $execution->scheduledDatabaseBackup->database->service->destination->server
} else { : $execution->scheduledDatabaseBackup->database->destination->server;
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
if ($this->delete_backup_s3) { try {
// Add logic to delete from S3 if ($execution->filename) {
} deleteBackupsLocally($execution->filename, $server);
if ($this->delete_backup_sftp) { if ($this->delete_backup_s3 && $execution->scheduledDatabaseBackup->s3) {
// Add logic to delete from SFTP deleteBackupsS3($execution->filename, $execution->scheduledDatabaseBackup->s3);
}
} }
$execution->delete(); $execution->delete();
$this->dispatch('success', 'Backup deleted.'); $this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions(); $this->refreshBackupExecutions();
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to delete backup: '.$e->getMessage());
}
} }
public function download_file($exeuctionId) public function download_file($exeuctionId)
@@ -83,8 +85,10 @@ class BackupExecutions extends Component
public function refreshBackupExecutions(): void public function refreshBackupExecutions(): void
{ {
if ($this->backup) { if ($this->backup && $this->backup->exists) {
$this->executions = $this->backup->executions()->get(); $this->executions = $this->backup->executions()->get()->toArray();
} else {
$this->executions = [];
} }
} }
@@ -113,35 +117,12 @@ class BackupExecutions extends Component
return null; return null;
} }
public function getServerTimezone()
{
$server = $this->server();
if (! $server) {
return 'UTC';
}
return $server->settings->server_timezone;
}
public function formatDateInServerTimezone($date)
{
$serverTimezone = $this->getServerTimezone();
$dateObj = new \DateTime($date);
try {
$dateObj->setTimezone(new \DateTimeZone($serverTimezone));
} catch (\Exception) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
public function render() public function render()
{ {
return view('livewire.project.database.backup-executions', [ return view('livewire.project.database.backup-executions', [
'checkboxes' => [ 'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'], ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'], // ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
], ],
]); ]);
} }

View File

@@ -9,11 +9,9 @@ class BackupNow extends Component
{ {
public $backup; public $backup;
public function backup_now() public function backupNow()
{ {
dispatch(new DatabaseBackupJob( DatabaseBackupJob::dispatch($this->backup);
backup: $this->backup
));
$this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); $this->dispatch('success', 'Backup queued. It will be available in a few minutes.');
} }
} }

View File

@@ -6,23 +6,34 @@ use Livewire\Component;
class Configuration extends Component class Configuration extends Component
{ {
public $currentRoute;
public $database; public $database;
public $project;
public $environment;
public function mount() public function mount()
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $this->currentRoute = request()->route()->getName();
if (! $project) {
return redirect()->route('dashboard'); $project = currentTeam()
} ->projects()
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']); ->select('id', 'uuid', 'team_id')
if (! $environment) { ->where('uuid', request()->route('project_uuid'))
return redirect()->route('dashboard'); ->firstOrFail();
} $environment = $project->environments()
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first(); ->select('id', 'name', 'project_id', 'uuid')
if (! $database) { ->where('uuid', request()->route('environment_uuid'))
return redirect()->route('dashboard'); ->firstOrFail();
} $database = $environment->databases()
->where('uuid', request()->route('database_uuid'))
->firstOrFail();
$this->database = $database; $this->database = $database;
$this->project = $project;
$this->environment = $environment;
if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) { if (str($this->database->status)->startsWith('running') && is_null($this->database->config_hash)) {
$this->database->isConfigurationChanged(true); $this->database->isConfigurationChanged(true);
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');

View File

@@ -43,8 +43,10 @@ class Heading extends Component
public function check_status($showNotification = false) public function check_status($showNotification = false)
{ {
GetContainersStatus::run($this->database->destination->server); if ($this->database->destination->server->isFunctional()) {
$this->database->refresh(); GetContainersStatus::dispatch($this->database->destination->server);
}
if ($showNotification) { if ($showNotification) {
$this->dispatch('success', 'Database status updated.'); $this->dispatch('success', 'Database status updated.');
} }

View File

@@ -37,6 +37,12 @@ class Import extends Component
public array $importCommands = []; public array $importCommands = [];
public bool $dumpAll = false;
public string $restoreCommandText = '';
public string $customLocation = '';
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB'; public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE'; public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
@@ -56,10 +62,62 @@ class Import extends Component
public function mount() public function mount()
{ {
if (isDev()) {
$this->customLocation = '/data/coolify/pg-dump-all-1736245863.gz';
}
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->getContainers(); $this->getContainers();
} }
public function updatedDumpAll($value)
{
switch ($this->resource->getMorphClass()) {
case \App\Models\StandaloneMariadb::class:
if ($value === true) {
$this->mariadbRestoreCommand = <<<'EOD'
for pid in $(mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
done && \
mariadb -u root -p$MARIADB_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXISTS \`',schema_name,'\`;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys');" | mariadb -u root -p$MARIADB_ROOT_PASSWORD && \
mariadb -u root -p$MARIADB_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`default\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mariadb -u root -p$MARIADB_ROOT_PASSWORD default
EOD;
$this->restoreCommandText = $this->mariadbRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mariadb -u root -p$MARIADB_ROOT_PASSWORD default';
} else {
$this->mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
}
break;
case \App\Models\StandaloneMysql::class:
if ($value === true) {
$this->mysqlRestoreCommand = <<<'EOD'
for pid in $(mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT id FROM information_schema.processlist WHERE user != 'root';"); do
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "KILL $pid" 2>/dev/null || true
done && \
mysql -u root -p$MYSQL_ROOT_PASSWORD -N -e "SELECT CONCAT('DROP DATABASE IF EXISTS \`',schema_name,'\`;') FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema','mysql','performance_schema','sys');" | mysql -u root -p$MYSQL_ROOT_PASSWORD && \
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS \`default\`;" && \
(gunzip -cf $tmpPath 2>/dev/null || cat $tmpPath) | sed -e '/^CREATE DATABASE/d' -e '/^USE \`mysql\`/d' | mysql -u root -p$MYSQL_ROOT_PASSWORD default
EOD;
$this->restoreCommandText = $this->mysqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | mysql -u root -p$MYSQL_ROOT_PASSWORD default';
} else {
$this->mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
}
break;
case \App\Models\StandalonePostgresql::class:
if ($value === true) {
$this->postgresqlRestoreCommand = <<<'EOD'
psql -U $POSTGRES_USER -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname IS NOT NULL AND pid <> pg_backend_pid()" && \
psql -U $POSTGRES_USER -t -c "SELECT datname FROM pg_database WHERE NOT datistemplate" | xargs -I {} dropdb -U $POSTGRES_USER --if-exists {} && \
createdb -U $POSTGRES_USER postgres
EOD;
$this->restoreCommandText = $this->postgresqlRestoreCommand.' && (gunzip -cf <temp_backup_file> 2>/dev/null || cat <temp_backup_file>) | psql -U $POSTGRES_USER postgres';
} else {
$this->postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
}
break;
}
}
public function getContainers() public function getContainers()
{ {
$this->containers = collect(); $this->containers = collect();
@@ -87,6 +145,24 @@ class Import extends Component
} }
} }
public function checkFile()
{
if (filled($this->customLocation)) {
try {
$result = instant_remote_process(["ls -l {$this->customLocation}"], $this->server, throwError: false);
if (blank($result)) {
$this->dispatch('error', 'The file does not exist or has been deleted.');
return;
}
$this->filename = $this->customLocation;
$this->dispatch('success', 'The file exists.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}
public function runImport() public function runImport()
{ {
if ($this->filename === '') { if ($this->filename === '') {
@@ -95,46 +171,83 @@ class Import extends Component
return; return;
} }
try { try {
$uploadedFilename = "upload/{$this->resource->uuid}/restore"; $this->importCommands = [];
$path = Storage::path($uploadedFilename); if (filled($this->customLocation)) {
if (! Storage::exists($uploadedFilename)) { $backupFileName = '/tmp/restore_'.$this->resource->uuid;
$this->importCommands[] = "docker cp {$this->customLocation} {$this->container}:{$backupFileName}";
$tmpPath = $backupFileName;
} else {
$backupFileName = "upload/{$this->resource->uuid}/restore";
$path = Storage::path($backupFileName);
if (! Storage::exists($backupFileName)) {
$this->dispatch('error', 'The file does not exist or has been deleted.'); $this->dispatch('error', 'The file does not exist or has been deleted.');
return; return;
} }
$tmpPath = '/tmp/'.basename($uploadedFilename); $tmpPath = '/tmp/'.basename($backupFileName).'_'.$this->resource->uuid;
instant_scp($path, $tmpPath, $this->server); instant_scp($path, $tmpPath, $this->server);
Storage::delete($uploadedFilename); Storage::delete($backupFileName);
$this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}";
}
// Copy the restore command to a script file
$scriptPath = "/tmp/restore_{$this->resource->uuid}.sh";
switch ($this->resource->getMorphClass()) { switch ($this->resource->getMorphClass()) {
case \App\Models\StandaloneMariadb::class: case \App\Models\StandaloneMariadb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'"; $restoreCommand = $this->mariadbRestoreCommand;
$this->importCommands[] = "rm {$tmpPath}"; if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mariadb -u root -p\$MARIADB_ROOT_PASSWORD";
} else {
$restoreCommand .= " < {$tmpPath}";
}
break; break;
case \App\Models\StandaloneMysql::class: case \App\Models\StandaloneMysql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'"; $restoreCommand = $this->mysqlRestoreCommand;
$this->importCommands[] = "rm {$tmpPath}"; if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | mysql -u root -p\$MYSQL_ROOT_PASSWORD";
} else {
$restoreCommand .= " < {$tmpPath}";
}
break; break;
case \App\Models\StandalonePostgresql::class: case \App\Models\StandalonePostgresql::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'"; $restoreCommand = $this->postgresqlRestoreCommand;
$this->importCommands[] = "rm {$tmpPath}"; if ($this->dumpAll) {
$restoreCommand .= " && (gunzip -cf {$tmpPath} 2>/dev/null || cat {$tmpPath}) | psql -U \$POSTGRES_USER postgres";
} else {
$restoreCommand .= " {$tmpPath}";
}
break; break;
case \App\Models\StandaloneMongodb::class: case \App\Models\StandaloneMongodb::class:
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'"; $restoreCommand = $this->mongodbRestoreCommand;
$this->importCommands[] = "rm {$tmpPath}"; if ($this->dumpAll === false) {
$restoreCommand .= "{$tmpPath}";
}
break; break;
} }
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'"; $restoreCommandBase64 = base64_encode($restoreCommand);
$this->importCommands[] = "echo \"{$restoreCommandBase64}\" | base64 -d > {$scriptPath}";
$this->importCommands[] = "chmod +x {$scriptPath}";
$this->importCommands[] = "docker cp {$scriptPath} {$this->container}:{$scriptPath}";
$this->importCommands[] = "docker exec {$this->container} sh -c '{$scriptPath}'";
$this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'"; $this->importCommands[] = "docker exec {$this->container} sh -c 'echo \"Import finished with exit code $?\"'";
if (! empty($this->importCommands)) { if (! empty($this->importCommands)) {
$activity = remote_process($this->importCommands, $this->server, ignore_errors: true); $activity = remote_process($this->importCommands, $this->server, ignore_errors: true, callEventOnFinish: 'RestoreJobFinished', callEventData: [
'scriptPath' => $scriptPath,
'tmpPath' => $tmpPath,
'container' => $this->container,
'serverId' => $this->server->id,
]);
$this->dispatch('activityMonitor', $activity->id); $this->dispatch('activityMonitor', $activity->id);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->filename = null;
$this->importCommands = [];
} }
} }
} }

View File

@@ -9,8 +9,6 @@ use App\Models\StandalonePostgresql;
use Exception; use Exception;
use Livewire\Component; use Livewire\Component;
use function Aws\filter;
class General extends Component class General extends Component
{ {
public StandalonePostgresql $database; public StandalonePostgresql $database;
@@ -126,10 +124,52 @@ class General extends Component
public function save_init_script($script) public function save_init_script($script)
{ {
$this->database->init_scripts = filter($this->database->init_scripts, fn ($s) => $s['filename'] !== $script['filename']); $initScripts = collect($this->database->init_scripts ?? []);
$this->database->init_scripts = array_merge($this->database->init_scripts, [$script]);
$existingScript = $initScripts->firstWhere('filename', $script['filename']);
$oldScript = $initScripts->firstWhere('index', $script['index']);
if ($existingScript && $existingScript['index'] !== $script['index']) {
$this->dispatch('error', 'A script with this filename already exists.');
return;
}
$container_name = $this->database->uuid;
$configuration_dir = database_configuration_dir().'/'.$container_name;
if ($oldScript && $oldScript['filename'] !== $script['filename']) {
$old_file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$oldScript['filename']}";
$delete_command = "rm -f $old_file_path";
try {
instant_remote_process([$delete_command], $this->server);
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to remove old init script from server: '.$e->getMessage());
return;
}
}
$index = $initScripts->search(function ($item) use ($script) {
return $item['index'] === $script['index'];
});
if ($index !== false) {
$initScripts[$index] = $script;
} else {
$initScripts->push($script);
}
$this->database->init_scripts = $initScripts->values()
->map(function ($item, $index) {
$item['index'] = $index;
return $item;
})
->all();
$this->database->save(); $this->database->save();
$this->dispatch('success', 'Init script saved.'); $this->dispatch('success', 'Init script saved and updated.');
} }
public function delete_init_script($script) public function delete_init_script($script)
@@ -137,13 +177,33 @@ class General extends Component
$collection = collect($this->database->init_scripts); $collection = collect($this->database->init_scripts);
$found = $collection->firstWhere('filename', $script['filename']); $found = $collection->firstWhere('filename', $script['filename']);
if ($found) { if ($found) {
$this->database->init_scripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])->toArray(); $container_name = $this->database->uuid;
$this->database->save(); $configuration_dir = database_configuration_dir().'/'.$container_name;
$this->refresh(); $file_path = "$configuration_dir/docker-entrypoint-initdb.d/{$script['filename']}";
$this->dispatch('success', 'Init script deleted.');
$command = "rm -f $file_path";
try {
instant_remote_process([$command], $this->server);
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to remove init script from server: '.$e->getMessage());
return; return;
} }
$updatedScripts = $collection->filter(fn ($s) => $s['filename'] !== $script['filename'])
->values()
->map(function ($item, $index) {
$item['index'] = $index;
return $item;
})
->all();
$this->database->init_scripts = $updatedScripts;
$this->database->save();
$this->refresh();
$this->dispatch('success', 'Init script deleted from the database and the server.');
}
} }
public function refresh(): void public function refresh(): void

View File

@@ -88,12 +88,12 @@ class General extends Component
if (version_compare($this->redis_version, '6.0', '>=')) { if (version_compare($this->redis_version, '6.0', '>=')) {
$this->database->runtime_environment_variables()->updateOrCreate( $this->database->runtime_environment_variables()->updateOrCreate(
['key' => 'REDIS_USERNAME'], ['key' => 'REDIS_USERNAME'],
['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] ['value' => $this->redis_username, 'resourceable_id' => $this->database->id]
); );
} }
$this->database->runtime_environment_variables()->updateOrCreate( $this->database->runtime_environment_variables()->updateOrCreate(
['key' => 'REDIS_PASSWORD'], ['key' => 'REDIS_PASSWORD'],
['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] ['value' => $this->redis_password, 'resourceable_id' => $this->database->id]
); );
$this->database->save(); $this->database->save();

View File

@@ -23,11 +23,11 @@ class EnvironmentEdit extends Component
#[Validate(['nullable', 'string', 'max:255'])] #[Validate(['nullable', 'string', 'max:255'])]
public ?string $description = null; public ?string $description = null;
public function mount(string $project_uuid, string $environment_name) public function mount(string $project_uuid, string $environment_uuid)
{ {
try { try {
$this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail(); $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail(); $this->environment = $this->project->environments()->where('uuid', $environment_uuid)->firstOrFail();
$this->syncData(); $this->syncData();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -52,7 +52,10 @@ class EnvironmentEdit extends Component
{ {
try { try {
$this->syncData(true); $this->syncData(true);
$this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]); $this->redirectRoute('project.environment.edit', [
'environment_uuid' => $this->environment->uuid,
'project_uuid' => $this->project->uuid,
]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -30,4 +30,11 @@ class Index extends Component
{ {
return view('livewire.project.index'); return view('livewire.project.index');
} }
public function navigateToProject($projectUuid)
{
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
return $this->redirect($project->navigateTo(), true);
}
} }

View File

@@ -52,14 +52,8 @@ class DockerCompose extends Component
'dockerComposeRaw' => 'required', 'dockerComposeRaw' => 'required',
]); ]);
$this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $this->dockerComposeRaw = Yaml::dump(Yaml::parse($this->dockerComposeRaw), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);
$isValid = validateComposeFile($this->dockerComposeRaw, $server_id);
if ($isValid !== 'OK') {
return $this->dispatch('error', "Invalid docker-compose file.\n$isValid");
}
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$destination_uuid = $this->query['destination']; $destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
@@ -87,7 +81,8 @@ class DockerCompose extends Component
'value' => $variable, 'value' => $variable,
'is_build_time' => false, 'is_build_time' => false,
'is_preview' => false, 'is_preview' => false,
'service_id' => $service->id, 'resourceable_id' => $service->id,
'resourceable_type' => $service->getMorphClass(),
]); ]);
} }
$service->name = "service-$service->uuid"; $service->name = "service-$service->uuid";
@@ -96,7 +91,7 @@ class DockerCompose extends Component
return redirect()->route('project.service.configuration', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\Project; use App\Models\Project;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use App\Services\DockerImageParser;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -28,12 +29,10 @@ class DockerImage extends Component
$this->validate([ $this->validate([
'dockerImage' => 'required', 'dockerImage' => 'required',
]); ]);
$image = str($this->dockerImage)->before(':');
if (str($this->dockerImage)->contains(':')) { $parser = new DockerImageParser;
$tag = str($this->dockerImage)->after(':'); $parser->parse($this->dockerImage);
} else {
$tag = 'latest';
}
$destination_uuid = $this->query['destination']; $destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) { if (! $destination) {
@@ -45,7 +44,7 @@ class DockerImage extends Component
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$application = Application::create([ $application = Application::create([
'name' => 'docker-image-'.new Cuid2, 'name' => 'docker-image-'.new Cuid2,
'repository_project_id' => 0, 'repository_project_id' => 0,
@@ -53,8 +52,8 @@ class DockerImage extends Component
'git_branch' => 'main', 'git_branch' => 'main',
'build_pack' => 'dockerimage', 'build_pack' => 'dockerimage',
'ports_exposes' => 80, 'ports_exposes' => 80,
'docker_registry_image_name' => $image, 'docker_registry_image_name' => $parser->getFullImageNameWithoutTag(),
'docker_registry_image_tag' => $tag, 'docker_registry_image_tag' => $parser->getTag(),
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination_class, 'destination_type' => $destination_class,
@@ -69,7 +68,7 @@ class DockerImage extends Component
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} }

View File

@@ -4,6 +4,7 @@ namespace App\Livewire\Project\New;
use App\Models\Project; use App\Models\Project;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class EmptyProject extends Component class EmptyProject extends Component
{ {
@@ -12,8 +13,9 @@ class EmptyProject extends Component
$project = Project::create([ $project = Project::create([
'name' => generate_random_name(), 'name' => generate_random_name(),
'team_id' => currentTeam()->id, 'team_id' => currentTeam()->id,
'uuid' => (string) new Cuid2,
]); ]);
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_name' => 'production']); return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
} }
} }

View File

@@ -105,7 +105,7 @@ class GithubPrivateRepository extends Component
$this->page = 1; $this->page = 1;
$this->selected_github_app_id = $github_app_id; $this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first(); $this->github_app = GithubApp::where('id', $github_app_id)->first();
$this->token = generate_github_installation_token($this->github_app); $this->token = generateGithubInstallationToken($this->github_app);
$this->loadRepositoryByPage(); $this->loadRepositoryByPage();
if ($this->repositories->count() < $this->total_repositories_count) { if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) { while ($this->repositories->count() < $this->total_repositories_count) {
@@ -177,7 +177,7 @@ class GithubPrivateRepository extends Component
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$application = Application::create([ $application = Application::create([
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name), 'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
@@ -211,7 +211,7 @@ class GithubPrivateRepository extends Component
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -136,7 +136,7 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->get_git_source(); $this->get_git_source();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
if ($this->git_source === 'other') { if ($this->git_source === 'other') {
$application_init = [ $application_init = [
'name' => generate_random_name(), 'name' => generate_random_name(),
@@ -184,7 +184,7 @@ class GithubPrivateRepositoryDeployKey extends Component
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -188,11 +188,22 @@ class PublicGitRepository extends Component
private function getGitSource() private function getGitSource()
{ {
$this->git_branch = 'main';
$this->base_directory = '/';
$this->repository_url_parsed = Url::fromString($this->repository_url); $this->repository_url_parsed = Url::fromString($this->repository_url);
$this->git_host = $this->repository_url_parsed->getHost(); $this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2);
if ($this->repository_url_parsed->getSegment(3) === 'tree') { if ($this->repository_url_parsed->getSegment(3) === 'tree') {
$this->git_branch = str($this->repository_url_parsed->getPath())->after('tree/')->value(); $path = str($this->repository_url_parsed->getPath())->trim('/');
$this->git_branch = str($path)->after('tree/')->before('/')->value();
$this->base_directory = str($path)->after($this->git_branch)->after('/')->value();
if (filled($this->base_directory)) {
$this->base_directory = '/'.$this->base_directory;
} else {
$this->base_directory = '/';
}
} else { } else {
$this->git_branch = 'main'; $this->git_branch = 'main';
} }
@@ -225,7 +236,7 @@ class PublicGitRepository extends Component
$this->validate(); $this->validate();
$destination_uuid = $this->query['destination']; $destination_uuid = $this->query['destination'];
$project_uuid = $this->parameters['project_uuid']; $project_uuid = $this->parameters['project_uuid'];
$environment_name = $this->parameters['environment_name']; $environment_uuid = $this->parameters['environment_uuid'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) { if (! $destination) {
@@ -237,7 +248,7 @@ class PublicGitRepository extends Component
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $project_uuid)->first(); $project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $environment_name)->first(); $environment = $project->load(['environments'])->environments->where('uuid', $environment_uuid)->first();
if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) { if ($this->build_pack === 'dockercompose' && isDev() && $this->new_compose_services) {
$server = $destination->server; $server = $destination->server;
@@ -260,7 +271,7 @@ class PublicGitRepository extends Component
return redirect()->route('project.service.configuration', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
@@ -319,7 +330,7 @@ class PublicGitRepository extends Component
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -23,6 +23,8 @@ class Select extends Component
public Collection|null|Server $servers; public Collection|null|Server $servers;
public bool $onlyBuildServerAvailable = false;
public ?Collection $standaloneDockers; public ?Collection $standaloneDockers;
public ?Collection $swarmDockers; public ?Collection $swarmDockers;
@@ -61,7 +63,7 @@ class Select extends Component
} }
$projectUuid = data_get($this->parameters, 'project_uuid'); $projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments; $this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name'); $this->selectedEnvironment = data_get($this->parameters, 'environment_uuid');
} }
public function render() public function render()
@@ -73,23 +75,13 @@ class Select extends Component
{ {
return redirect()->route('project.resource.create', [ return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment, 'environment_uuid' => $this->selectedEnvironment,
]); ]);
} }
// public function addExistingPostgresql()
// {
// try {
// instantCommand("psql {$this->existingPostgresqlUrl} -c 'SELECT 1'");
// $this->dispatch('success', 'Successfully connected to the database.');
// } catch (\Throwable $e) {
// return handleError($e, $this);
// }
// }
public function loadServices() public function loadServices()
{ {
$services = get_service_templates(true); $services = get_service_templates();
$services = collect($services)->map(function ($service, $key) { $services = collect($services)->map(function ($service, $key) {
$default_logo = 'images/default.webp'; $default_logo = 'images/default.webp';
$logo = data_get($service, 'logo', $default_logo); $logo = data_get($service, 'logo', $default_logo);
@@ -308,7 +300,7 @@ class Select extends Component
return redirect()->route('project.resource.create', [ return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
'type' => $this->type, 'type' => $this->type,
'destination' => $this->destination_uuid, 'destination' => $this->destination_uuid,
'server_id' => $this->server_id, 'server_id' => $this->server_id,
@@ -323,7 +315,7 @@ class Select extends Component
} else { } else {
return redirect()->route('project.resource.create', [ return redirect()->route('project.resource.create', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name'], 'environment_uuid' => $this->parameters['environment_uuid'],
'type' => $this->type, 'type' => $this->type,
'destination' => $this->destination_uuid, 'destination' => $this->destination_uuid,
'server_id' => $this->server_id, 'server_id' => $this->server_id,
@@ -335,5 +327,11 @@ class Select extends Component
{ {
$this->servers = Server::isUsable()->get()->sortBy('name'); $this->servers = Server::isUsable()->get()->sortBy('name');
$this->allServers = $this->servers; $this->allServers = $this->servers;
if ($this->allServers && $this->allServers->isNotEmpty()) {
$this->onlyBuildServerAvailable = $this->allServers->every(function ($server) {
return $server->isBuildServer();
});
}
} }
} }

View File

@@ -46,7 +46,7 @@ CMD ["nginx", "-g", "daemon off;"]
$destination_class = $destination->getMorphClass(); $destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
$port = get_port_from_dockerfile($this->dockerfile); $port = get_port_from_dockerfile($this->dockerfile);
if (! $port) { if (! $port) {
@@ -78,7 +78,7 @@ CMD ["nginx", "-g", "daemon off;"]
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid, 'application_uuid' => $application->uuid,
'environment_name' => $environment->name, 'environment_uuid' => $environment->uuid,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,
]); ]);
} }

Some files were not shown because too many files have changed in this diff Show More